TYPO3 v15 dev-main snapshot ()
@@ -0,0 +1 @@
|
||||
/vendor/
|
||||
@@ -0,0 +1,259 @@
|
||||
<?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\Scheduler\Command;
|
||||
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use TYPO3\CMS\Core\Attribute\AsNonSchedulableCommand;
|
||||
|
||||
use TYPO3\CMS\Core\Core\Bootstrap;
|
||||
use TYPO3\CMS\Core\Utility\MathUtility;
|
||||
use TYPO3\CMS\Scheduler\Domain\Repository\SchedulerTaskRepository;
|
||||
use TYPO3\CMS\Scheduler\Scheduler;
|
||||
use TYPO3\CMS\Scheduler\Service\TaskService;
|
||||
use TYPO3\CMS\Scheduler\Task\AbstractTask;
|
||||
use TYPO3\CMS\Scheduler\Validation\Validator\TaskValidator;
|
||||
|
||||
/**
|
||||
* CLI command for the 'scheduler' extension which executes
|
||||
*
|
||||
* @internal Specific command implementation, not part of TYPO3 API.
|
||||
*/
|
||||
#[AsCommand('scheduler:run', 'Start the TYPO3 Scheduler from the command line.')]
|
||||
#[AsNonSchedulableCommand]
|
||||
class SchedulerCommand extends Command
|
||||
{
|
||||
/**
|
||||
* @var SymfonyStyle
|
||||
*/
|
||||
protected $io;
|
||||
|
||||
/**
|
||||
* Array of tasks UIDs that should be executed. Null if task option is not provided.
|
||||
*
|
||||
* @var int[]|null
|
||||
*/
|
||||
protected $overwrittenTaskList;
|
||||
|
||||
/**
|
||||
* This is true when the tasks should be marked as stopped instead of being executed.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $stopTasks = false;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
protected $forceExecution;
|
||||
|
||||
public function __construct(
|
||||
protected readonly Scheduler $scheduler,
|
||||
protected readonly SchedulerTaskRepository $taskRepository,
|
||||
protected readonly TaskService $taskService,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the command by defining the name, options and arguments
|
||||
*/
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setHelp('If no parameter is given, the scheduler executes any tasks that are overdue to run.
|
||||
Call it like this: typo3/sysext/core/bin/typo3 scheduler:run --task=13 -f')
|
||||
->addOption(
|
||||
'task',
|
||||
'i',
|
||||
InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY,
|
||||
'UID of a specific task. Can be provided multiple times to execute multiple tasks sequentially.'
|
||||
)
|
||||
->addOption(
|
||||
'force',
|
||||
'f',
|
||||
InputOption::VALUE_NONE,
|
||||
'Force execution of the task which is passed with --task option'
|
||||
)
|
||||
->addOption(
|
||||
'stop',
|
||||
's',
|
||||
InputOption::VALUE_NONE,
|
||||
'Stop the task which is passed with --task option'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute scheduler tasks
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$this->io = new SymfonyStyle($input, $output);
|
||||
|
||||
// Make sure the _cli_ user is loaded
|
||||
Bootstrap::initializeBackendAuthentication();
|
||||
|
||||
$overwrittenTaskList = $input->getOption('task');
|
||||
$overwrittenTaskList = is_array($overwrittenTaskList) ? $overwrittenTaskList : [];
|
||||
$overwrittenTaskList = array_filter($overwrittenTaskList, static fn($value) => MathUtility::canBeInterpretedAsInteger($value));
|
||||
$overwrittenTaskList = array_map('intval', $overwrittenTaskList);
|
||||
if ($overwrittenTaskList !== []) {
|
||||
$this->overwrittenTaskList = $overwrittenTaskList;
|
||||
}
|
||||
|
||||
$this->forceExecution = (bool)$input->getOption('force');
|
||||
$this->stopTasks = $this->shouldStopTasks((bool)$input->getOption('stop'));
|
||||
return $this->loopTasks() ? Command::SUCCESS : Command::FAILURE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the tasks should be stopped instead of being executed.
|
||||
*
|
||||
* Stopping is only performed when the --stop option is provided together with the --task option.
|
||||
*
|
||||
* @param bool $stopOption
|
||||
*/
|
||||
protected function shouldStopTasks(bool $stopOption): bool
|
||||
{
|
||||
if (!$stopOption) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->overwrittenTaskList !== []) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($this->io->isVerbose()) {
|
||||
$this->io->warning('Stopping tasks is only possible when the --task option is provided.');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop task
|
||||
*/
|
||||
protected function stopTask(AbstractTask $task)
|
||||
{
|
||||
$this->taskRepository->removeAllRegisteredExecutionsForTask($task);
|
||||
if ($this->io->isVeryVerbose()) {
|
||||
$this->io->writeln(sprintf('Task #%d was stopped', $task->getTaskUid()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return task a task for a given UID
|
||||
*/
|
||||
protected function getTask(int $taskUid): ?AbstractTask
|
||||
{
|
||||
$force = $this->stopTasks || $this->forceExecution;
|
||||
if ($force) {
|
||||
return $this->taskRepository->findByUid($taskUid);
|
||||
}
|
||||
|
||||
return $this->taskRepository->findNextExecutableTaskForUid($taskUid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute tasks in loop that are ready to execute
|
||||
*/
|
||||
protected function loopTasks(): bool
|
||||
{
|
||||
$hasError = false;
|
||||
do {
|
||||
$task = null;
|
||||
// Try getting the next task and execute it
|
||||
// If there are no more tasks to execute, an exception is thrown by \TYPO3\CMS\Scheduler\Scheduler::fetchTask()
|
||||
try {
|
||||
$task = $this->fetchNextTask();
|
||||
if ($task === null) {
|
||||
break;
|
||||
}
|
||||
try {
|
||||
$this->executeOrStopTask($task);
|
||||
} catch (\Exception $e) {
|
||||
$taskDetails = $this->taskService->getTaskDetailsFromTask($task);
|
||||
$messages = [
|
||||
$e->getMessage() . PHP_EOL,
|
||||
'Exception in scheduler task #' . $task->getTaskUid() . ' (' . $task->getTaskType() . ' - ' . $taskDetails['title'] . ')',
|
||||
];
|
||||
$messages[] = 'File: ' . $e->getFile() . ':' . $e->getLine();
|
||||
$this->io->getErrorStyle()->error($messages);
|
||||
$hasError = true;
|
||||
// We ignore any exception that may have been thrown during execution,
|
||||
// as this is a background process.
|
||||
// The exception message has been recorded to the database anyway
|
||||
continue;
|
||||
}
|
||||
} catch (\UnexpectedValueException $e) {
|
||||
$this->io->getErrorStyle()->error($e->getMessage());
|
||||
$hasError = true;
|
||||
continue;
|
||||
}
|
||||
} while ($task !== null);
|
||||
// Record the run in the system registry
|
||||
$this->scheduler->recordLastRun();
|
||||
return !$hasError;
|
||||
}
|
||||
|
||||
/**
|
||||
* When the --task option is provided, the next task is fetched from the provided task UIDs. Depending
|
||||
* on the --force option the task is fetched even if it is not marked for execution.
|
||||
*
|
||||
* Without the --task option we ask the scheduler for the next task with pending execution.
|
||||
*
|
||||
* @throws \UnexpectedValueException When no task is found by the provided UID or the task is not marked for execution.
|
||||
*/
|
||||
protected function fetchNextTask(): ?AbstractTask
|
||||
{
|
||||
if ($this->overwrittenTaskList === null) {
|
||||
return $this->taskRepository->findNextExecutableTask();
|
||||
}
|
||||
|
||||
if (count($this->overwrittenTaskList) === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$taskUid = (int)array_shift($this->overwrittenTaskList);
|
||||
$task = $this->getTask($taskUid);
|
||||
if (!(new TaskValidator())->isValid($task)) {
|
||||
throw new \UnexpectedValueException(
|
||||
sprintf('The task #%d is not scheduled for execution or does not exist.', $taskUid),
|
||||
1547675557
|
||||
);
|
||||
}
|
||||
return $task;
|
||||
}
|
||||
|
||||
/**
|
||||
* When in stop mode the given task is stopped. Otherwise the task is executed.
|
||||
*/
|
||||
protected function executeOrStopTask(AbstractTask $task): void
|
||||
{
|
||||
if ($this->stopTasks) {
|
||||
$this->stopTask($task);
|
||||
return;
|
||||
}
|
||||
|
||||
$this->scheduler->executeTask($task);
|
||||
if ($this->io->isVeryVerbose()) {
|
||||
$this->io->writeln(sprintf('Task #%d was executed', $task->getTaskUid()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
<?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\Command;
|
||||
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Helper\QuestionHelper;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Question\ChoiceQuestion;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use TYPO3\CMS\Core\Attribute\AsNonSchedulableCommand;
|
||||
|
||||
use TYPO3\CMS\Core\Context\Context;
|
||||
use TYPO3\CMS\Core\Core\Bootstrap;
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
use TYPO3\CMS\Core\Localization\LanguageServiceFactory;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Scheduler\Domain\Repository\SchedulerTaskRepository;
|
||||
use TYPO3\CMS\Scheduler\Scheduler;
|
||||
use TYPO3\CMS\Scheduler\Service\TaskService;
|
||||
|
||||
/**
|
||||
* CLI command for EXT:scheduler to execute tasks
|
||||
*/
|
||||
#[AsCommand('scheduler:execute', 'Execute given TYPO3 Scheduler tasks.')]
|
||||
#[AsNonSchedulableCommand]
|
||||
class SchedulerExecuteCommand extends Command
|
||||
{
|
||||
protected SymfonyStyle $io;
|
||||
|
||||
public function __construct(
|
||||
protected readonly Context $context,
|
||||
protected readonly SchedulerTaskRepository $taskRepository,
|
||||
protected readonly TaskService $taskService,
|
||||
protected Scheduler $scheduler,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->addOption(
|
||||
'task',
|
||||
't',
|
||||
InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY,
|
||||
'Execute tasks by given id. To run all tasks of a group prefix the group id with "g:", e.g. "g:1"',
|
||||
);
|
||||
}
|
||||
|
||||
public function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
// Make sure the _cli_ user is loaded
|
||||
Bootstrap::initializeBackendAuthentication();
|
||||
$this->io = new SymfonyStyle($input, $output);
|
||||
|
||||
if (count($input->getOption('task')) > 0) {
|
||||
$taskGroups = $this->taskRepository->getGroupedTasks()['taskGroupsWithTasks'];
|
||||
$tasksToRun = $this->getTasksToRun($taskGroups, $input->getOption('task'));
|
||||
$this->runTasks($tasksToRun, $taskGroups);
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
$this->askForTasksAndRun($input, $output);
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
private function askForTasksAndRun(InputInterface $input, OutputInterface $output): void
|
||||
{
|
||||
/** @var QuestionHelper $questionHelper */
|
||||
$questionHelper = $this->getHelper('question');
|
||||
$taskGroups = $this->taskRepository->getGroupedTasks()['taskGroupsWithTasks'];
|
||||
$selectableTasks = $this->getSelectableTasks($taskGroups);
|
||||
if ($selectableTasks === []) {
|
||||
$this->io->note('No tasks available.');
|
||||
return;
|
||||
}
|
||||
|
||||
$tasksToRunQuestion = new ChoiceQuestion('Run tasks (comma-separated list): ', $selectableTasks);
|
||||
$tasksToRunQuestion->setAutocompleterValues(array_keys($selectableTasks));
|
||||
$tasksToRunQuestion->setMultiselect(true);
|
||||
$tasksToRun = $questionHelper->ask($input, $output, $tasksToRunQuestion);
|
||||
|
||||
$this->runTasks($tasksToRun, $taskGroups);
|
||||
}
|
||||
|
||||
private function runTasks($selectedTasks, $taskGroups): void
|
||||
{
|
||||
$taskUids = $this->getTaskUidsFromSelection($selectedTasks, $taskGroups);
|
||||
ksort($taskUids);
|
||||
$numLength = strlen((string)array_reverse($taskUids)[0]);
|
||||
|
||||
foreach ($taskUids as $taskUid) {
|
||||
try {
|
||||
$uid = (int)$taskUid;
|
||||
$task = $this->taskRepository->findByUid($uid);
|
||||
$additionalInformation = $task->getAdditionalInformation() === '' ? '' : ' (' . $task->getAdditionalInformation() . ')';
|
||||
$taskDetails = $this->taskService->getTaskDetailsFromTask($task);
|
||||
$space = str_repeat(' ', $numLength - strlen((string)$task->getTaskUid()));
|
||||
$this->io->writeln('[ <fg=green>TASK:' . $task->getTaskUid() . $space . '</> ] Running "' . $taskDetails['title'] . $additionalInformation . '"');
|
||||
$this->scheduler->executeTask($task);
|
||||
} catch (\Throwable $exception) {
|
||||
$this->io->writeln($exception->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function getTaskUidsFromSelection(array $list, array $groups): array
|
||||
{
|
||||
$taskUids = [];
|
||||
foreach ($list as $uid) {
|
||||
[$keyword, $group] = [...explode(':', (string)$uid), null];
|
||||
if ($keyword === 'g') {
|
||||
if (!array_key_exists($group, $groups)) {
|
||||
throw new \InvalidArgumentException('Group with id "' . $group . '" does not exist.', 1679683415);
|
||||
}
|
||||
$taskUidsInGroup = array_column($groups[$group]['tasks'], 'uid');
|
||||
$taskUids += $taskUidsInGroup;
|
||||
} else {
|
||||
$taskUids[] = $uid;
|
||||
}
|
||||
}
|
||||
return $taskUids;
|
||||
}
|
||||
|
||||
private function getLanguageService(): LanguageService
|
||||
{
|
||||
return GeneralUtility::makeInstance(LanguageServiceFactory::class)->create('en');
|
||||
}
|
||||
|
||||
private function getTasksToRun(array $taskGroups, array $taskList): array
|
||||
{
|
||||
$taskUids = array_unique($this->getTaskUidsFromSelection($taskList, $taskGroups));
|
||||
foreach ($taskUids as $taskUid) {
|
||||
// This will throw an exception if the task uid was not found and print it to the console.
|
||||
$this->taskRepository->findByUid((int)$taskUid);
|
||||
}
|
||||
return $taskUids;
|
||||
}
|
||||
|
||||
protected function getSelectableTasks(mixed $taskGroups): array
|
||||
{
|
||||
$selectableTasks = [];
|
||||
foreach ($taskGroups as $uid => $group) {
|
||||
$groupLabel = ($group['groupName'] ?? $this->getLanguageService()->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:label.noGroup'));
|
||||
$selectableTasks['g:' . $uid] = '<fg=yellow>' . $groupLabel . '</>';
|
||||
|
||||
foreach ($group['tasks'] as $task) {
|
||||
$additionalInformation = $task['additionalInformation'] === '' ? '' : ' (' . $task['additionalInformation'] . ')';
|
||||
$selectableTasks[$task['uid']] = $task['fullTitle'] . $additionalInformation;
|
||||
}
|
||||
}
|
||||
return $selectableTasks;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
<?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\Command;
|
||||
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Helper\Table;
|
||||
use Symfony\Component\Console\Helper\TableCell;
|
||||
use Symfony\Component\Console\Helper\TableSeparator;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\BufferedOutput;
|
||||
use Symfony\Component\Console\Output\ConsoleOutputInterface;
|
||||
use Symfony\Component\Console\Output\ConsoleSectionOutput;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use TYPO3\CMS\Core\Attribute\AsNonSchedulableCommand;
|
||||
use TYPO3\CMS\Core\Core\Bootstrap;
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
use TYPO3\CMS\Core\Localization\LanguageServiceFactory;
|
||||
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Scheduler\Domain\Repository\SchedulerTaskRepository;
|
||||
use TYPO3\CMS\Scheduler\Task\TaskStatus;
|
||||
|
||||
/**
|
||||
* CLI command for EXT:scheduler to list tasks
|
||||
*/
|
||||
#[AsCommand('scheduler:list', 'List all TYPO3 Scheduler tasks.')]
|
||||
#[AsNonSchedulableCommand]
|
||||
class SchedulerListCommand extends Command
|
||||
{
|
||||
protected SymfonyStyle $io;
|
||||
|
||||
public function __construct(
|
||||
protected readonly SchedulerTaskRepository $taskRepository,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->addOption(
|
||||
'group',
|
||||
'g',
|
||||
InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY,
|
||||
'Show only groups with given uid',
|
||||
)
|
||||
->addOption(
|
||||
'watch',
|
||||
'w',
|
||||
InputOption::VALUE_OPTIONAL,
|
||||
'Start watcher mode (polling)',
|
||||
);
|
||||
}
|
||||
|
||||
public function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
if (!$output instanceof ConsoleOutputInterface) {
|
||||
throw new \InvalidArgumentException('This command accepts only an instance of "ConsoleOutputInterface".', 1678645754);
|
||||
}
|
||||
|
||||
// Make sure the _cli_ user is loaded
|
||||
Bootstrap::initializeBackendAuthentication();
|
||||
$this->io = new SymfonyStyle($input, $output);
|
||||
$languageService = $this->getLanguageService();
|
||||
|
||||
$tableHeader = [
|
||||
$languageService->sL('scheduler.messages:label.id'),
|
||||
$languageService->sL('scheduler.messages:task'),
|
||||
$languageService->sL('scheduler.messages:label.description'),
|
||||
$languageService->sL('scheduler.messages:label.frequency'),
|
||||
$languageService->sL('scheduler.messages:status'),
|
||||
];
|
||||
|
||||
$tableSection = $output->section();
|
||||
$tableBuffer = new BufferedOutput(OutputInterface::VERBOSITY_NORMAL, true);
|
||||
$table = new Table($tableBuffer);
|
||||
$table->setHeaders($tableHeader);
|
||||
$showGroups = $input->getOption('group');
|
||||
$rows = $this->getTableRows($showGroups);
|
||||
$table->setRows($rows);
|
||||
$table->setColumnMaxWidth(1, 50);
|
||||
$table->setColumnMaxWidth(2, 50);
|
||||
$table->setColumnMaxWidth(4, 50);
|
||||
$table->render();
|
||||
|
||||
$bufferedData = $tableBuffer->fetch();
|
||||
$tableSection->overwrite($bufferedData);
|
||||
|
||||
$doWatch = $input->hasParameterOption('--watch') || $input->hasParameterOption('-w');
|
||||
if ($doWatch) {
|
||||
$infoSection = $output->section();
|
||||
$interval = (int)($input->getOption('watch') ?: 1);
|
||||
$infoSection->write('Watching tasks every ' . $interval . ' seconds, press CTRL+C to stop watching');
|
||||
|
||||
while (true) { // @phpstan-ignore while.alwaysTrue (intentional infinite loop for CLI watch mode, terminated by CTRL+C signal)
|
||||
sleep($interval);
|
||||
$this->updateTable($input, $table, $tableBuffer, $tableSection);
|
||||
}
|
||||
}
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
private function getTableRows(array $groups = []): array
|
||||
{
|
||||
$tasks = $this->taskRepository->getGroupedTasks();
|
||||
$languageService = $this->getLanguageService();
|
||||
|
||||
$rows = [];
|
||||
foreach ($tasks['taskGroupsWithTasks'] as $uid => $group) {
|
||||
if (!in_array($uid, $groups) && count($groups) > 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Flag as disabled group
|
||||
$groupDisabledLabel = $group['hidden'] ? '<fg=yellow>' . $this->getLanguageService()->sL('scheduler.messages:status.disabled') . '</>' : '';
|
||||
$groupLabel = ($group['groupName'] ?? $languageService->sL('scheduler.messages:label.noGroup')) . ' (id:' . $uid . ') ' . $groupDisabledLabel;
|
||||
|
||||
$rows[] = [new TableSeparator(['colspan' => 5])];
|
||||
$rows[] = [new TableCell('<options=bold>' . $groupLabel . '</>', ['colspan' => 5])];
|
||||
$rows[] = [new TableSeparator(['colspan' => 5])];
|
||||
|
||||
foreach ($group['tasks'] as $task) {
|
||||
$progress = $task['progress'] ?? false;
|
||||
$taskStatus = [];
|
||||
|
||||
/** @var TaskStatus $status */
|
||||
foreach ($task['statuses'] as $status) {
|
||||
$color = match ($status->severity) {
|
||||
ContextualFeedbackSeverity::OK => 'green',
|
||||
ContextualFeedbackSeverity::INFO => 'blue',
|
||||
ContextualFeedbackSeverity::WARNING => 'yellow',
|
||||
ContextualFeedbackSeverity::ERROR => 'red',
|
||||
ContextualFeedbackSeverity::NOTICE => 'gray',
|
||||
};
|
||||
$label = $languageService->sL($status->label);
|
||||
if ($status->type === 'running' && $progress) {
|
||||
$label .= ' (' . $progress . ')';
|
||||
}
|
||||
// The console has no tooltip, so the more detailed message
|
||||
// (e.g. the failure reason) is shown inline instead.
|
||||
if ($status->message !== '') {
|
||||
$label .= ' (' . vsprintf($languageService->sL($status->message), $status->messageArguments) . ')';
|
||||
}
|
||||
$taskStatus[] = '<fg=' . $color . '>' . $label . '</>';
|
||||
}
|
||||
|
||||
$taskTitle = $task['fullTitle'] . (empty($task['additionalInformation']) ? '' : ' (' . $task['additionalInformation'] . ')');
|
||||
$rows[] = [
|
||||
$task['uid'],
|
||||
$taskTitle,
|
||||
$task['description'],
|
||||
$task['frequency'],
|
||||
implode(', ', $taskStatus),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
private function getLanguageService(): LanguageService
|
||||
{
|
||||
return GeneralUtility::makeInstance(LanguageServiceFactory::class)->create('en');
|
||||
}
|
||||
|
||||
protected function updateTable(InputInterface $input, Table $table, BufferedOutput $buffer, ConsoleSectionOutput $tableSection): void
|
||||
{
|
||||
$tableSection->overwrite('');
|
||||
$rows = $this->getTableRows($input->getOption('group'));
|
||||
$table->setRows($rows)->render();
|
||||
$bufferedData = $buffer->fetch();
|
||||
$tableSection->overwrite($bufferedData);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
<?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\Controller;
|
||||
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Backend\Attribute\AsController;
|
||||
use TYPO3\CMS\Backend\Routing\UriBuilder;
|
||||
use TYPO3\CMS\Backend\View\BackendViewFactory;
|
||||
use TYPO3\CMS\Core\Http\HtmlResponse;
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Scheduler\Event\ModifyNewSchedulerTaskWizardItemsEvent;
|
||||
use TYPO3\CMS\Scheduler\Service\TaskService;
|
||||
|
||||
/**
|
||||
* New Scheduler Task wizard. This is the modal that pops up when clicking "New Task" in scheduler module.
|
||||
*
|
||||
* @internal This class is a specific Backend controller implementation and is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
#[AsController]
|
||||
class NewSchedulerTaskController
|
||||
{
|
||||
protected string $returnUrl = '';
|
||||
protected array $defaultValues = [];
|
||||
|
||||
public function __construct(
|
||||
protected readonly UriBuilder $uriBuilder,
|
||||
protected readonly BackendViewFactory $backendViewFactory,
|
||||
protected readonly EventDispatcherInterface $eventDispatcher,
|
||||
protected readonly TaskService $taskService,
|
||||
) {}
|
||||
|
||||
public function handleRequest(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$this->returnUrl = GeneralUtility::sanitizeLocalUrl((string)($request->getParsedBody()['returnUrl'] ?? $request->getQueryParams()['returnUrl'] ?? ''), $request);
|
||||
$this->defaultValues = $request->getParsedBody()['defaultValues'] ?? $request->getQueryParams()['defaultValues'] ?? [];
|
||||
|
||||
$wizardItems = $this->eventDispatcher->dispatch(
|
||||
new ModifyNewSchedulerTaskWizardItemsEvent(
|
||||
$this->getWizardItemsFromTaskRegistry(),
|
||||
$request,
|
||||
)
|
||||
)->getWizardItems();
|
||||
|
||||
$view = $this->backendViewFactory->create($request);
|
||||
$view->assign('categoriesJson', GeneralUtility::jsonEncodeForHtmlAttribute($this->organizeWizardItems($wizardItems), false));
|
||||
return new HtmlResponse($view->render('NewSchedulerTask/Wizard'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert TaskService categorized tasks to wizard items
|
||||
*/
|
||||
protected function getWizardItemsFromTaskRegistry(): array
|
||||
{
|
||||
$wizardItems = [];
|
||||
|
||||
foreach ($this->taskService->getCategorizedTaskTypes() as $category => $tasks) {
|
||||
// Add category header
|
||||
$wizardItems[$category] = [
|
||||
'header' => ucfirst($category),
|
||||
];
|
||||
// Add tasks for this category
|
||||
foreach ($tasks as $taskType => $taskInfo) {
|
||||
$wizardItems[$category . '_' . str_replace('\\', '_', $taskType)] = [
|
||||
'title' => $taskInfo['title'],
|
||||
'description' => $taskInfo['description'],
|
||||
// @todo change once tx_scheduler_task get's icons
|
||||
'icon' => $taskInfo['icon'] ?? 'mimetypes-x-tx_scheduler_task_group',
|
||||
'iconOverlay' => $taskInfo['iconOverlay'] ?? '',
|
||||
'taskType' => $taskType,
|
||||
'taskClass' => $taskInfo['className'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $wizardItems;
|
||||
}
|
||||
|
||||
/**
|
||||
* Organize wizard items into the categories
|
||||
*/
|
||||
protected function organizeWizardItems(array $wizardItems): array
|
||||
{
|
||||
$categories = [];
|
||||
$currentKey = '';
|
||||
|
||||
foreach ($wizardItems as $wizardKey => $wizardItem) {
|
||||
if (isset($wizardItem['header'])) {
|
||||
// This is a category
|
||||
$currentKey = $wizardKey;
|
||||
$categories[$currentKey] = [
|
||||
'identifier' => $currentKey,
|
||||
'label' => $wizardItem['header'],
|
||||
'items' => [],
|
||||
];
|
||||
} else {
|
||||
if (!($wizardItem['taskType'] ?? false)) {
|
||||
continue;
|
||||
}
|
||||
// This is a task item
|
||||
$item = [
|
||||
'identifier' => $wizardKey,
|
||||
'icon' => $wizardItem['icon'] ?? 'mimetypes-x-tx_scheduler_task_group',
|
||||
'iconOverlay' => $wizardItem['iconOverlay'] ?? '',
|
||||
'label' => $wizardItem['title'] ?? '',
|
||||
'description' => $wizardItem['description'] ?? '',
|
||||
'taskType' => $wizardItem['taskType'],
|
||||
];
|
||||
|
||||
$item['url'] = (string)$this->uriBuilder->buildUriFromRoute('record_edit', [
|
||||
'edit' => [
|
||||
'tx_scheduler_task' => [
|
||||
0 => 'new',
|
||||
],
|
||||
],
|
||||
'defVals' => [
|
||||
'tx_scheduler_task' => array_replace_recursive($this->defaultValues, [
|
||||
'pid' => 0,
|
||||
'tasktype' => $wizardItem['taskType'],
|
||||
]),
|
||||
],
|
||||
'returnUrl' => $this->returnUrl,
|
||||
]);
|
||||
|
||||
if (!empty($currentKey)) {
|
||||
$categories[$currentKey]['items'][] = $item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove empty categories
|
||||
return array_filter($categories, static fn(array $category): bool => !empty($category['items']));
|
||||
}
|
||||
|
||||
protected function getLanguageService(): LanguageService
|
||||
{
|
||||
return $GLOBALS['LANG'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,509 @@
|
||||
<?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\Controller;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Backend\Attribute\AsController;
|
||||
use TYPO3\CMS\Backend\Module\ModuleData;
|
||||
use TYPO3\CMS\Backend\Routing\UriBuilder;
|
||||
use TYPO3\CMS\Backend\Template\Components\ButtonBar;
|
||||
use TYPO3\CMS\Backend\Template\Components\ComponentFactory;
|
||||
use TYPO3\CMS\Backend\Template\ModuleTemplate;
|
||||
use TYPO3\CMS\Backend\Template\ModuleTemplateFactory;
|
||||
use TYPO3\CMS\Core\Context\Context;
|
||||
use TYPO3\CMS\Core\Core\Environment;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\Database\Query\Restriction\HiddenRestriction;
|
||||
use TYPO3\CMS\Core\Imaging\IconFactory;
|
||||
use TYPO3\CMS\Core\Imaging\IconSize;
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
use TYPO3\CMS\Core\Page\PageRenderer;
|
||||
use TYPO3\CMS\Core\Registry;
|
||||
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\View\ViewInterface;
|
||||
use TYPO3\CMS\Scheduler\Domain\Repository\SchedulerTaskRepository;
|
||||
use TYPO3\CMS\Scheduler\Execution;
|
||||
use TYPO3\CMS\Scheduler\Scheduler;
|
||||
use TYPO3\CMS\Scheduler\Service\TaskService;
|
||||
|
||||
/**
|
||||
* Scheduler backend module.
|
||||
*
|
||||
* @internal This class is a specific Backend controller implementation and is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
#[AsController]
|
||||
final readonly class SchedulerModuleController
|
||||
{
|
||||
public function __construct(
|
||||
private Scheduler $scheduler,
|
||||
private SchedulerTaskRepository $taskRepository,
|
||||
private IconFactory $iconFactory,
|
||||
private UriBuilder $uriBuilder,
|
||||
private ModuleTemplateFactory $moduleTemplateFactory,
|
||||
private ComponentFactory $componentFactory,
|
||||
private Context $context,
|
||||
private TaskService $taskService,
|
||||
private PageRenderer $pageRenderer,
|
||||
private Registry $registry,
|
||||
private ConnectionPool $connectionPool,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Entry dispatcher method.
|
||||
*/
|
||||
public function handleRequest(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$parsedBody = $request->getParsedBody();
|
||||
|
||||
$view = $this->moduleTemplateFactory->create($request);
|
||||
$view->assign('dateFormat', [
|
||||
'day' => $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'] ?? 'd-m-y',
|
||||
'time' => $GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'] ?? 'H:i',
|
||||
]);
|
||||
|
||||
$moduleData = $request->getAttribute('moduleData');
|
||||
|
||||
// Simple actions from list view.
|
||||
if (!empty($parsedBody['action']['toggleHidden'])) {
|
||||
$this->toggleDisabledFlag($view, (int)$parsedBody['action']['toggleHidden']);
|
||||
} elseif (!empty($parsedBody['action']['stop'])) {
|
||||
$this->stopTask($view, (int)$parsedBody['action']['stop']);
|
||||
} elseif (!empty($parsedBody['action']['execute'])) {
|
||||
$this->executeTasks($view, (string)$parsedBody['action']['execute']);
|
||||
} elseif (!empty($parsedBody['action']['scheduleCron'])) {
|
||||
$this->scheduleCrons($view, (string)$parsedBody['action']['scheduleCron']);
|
||||
} elseif (!empty($parsedBody['action']['group']['uid'])) {
|
||||
$this->groupDisable((int)$parsedBody['action']['group']['uid'], (int)($parsedBody['action']['group']['hidden'] ?? 0));
|
||||
} elseif (!empty($parsedBody['action']['delete'])) {
|
||||
$this->deleteTask($view, (int)$parsedBody['action']['delete']);
|
||||
} elseif (!empty($parsedBody['action']['groupRemove'])) {
|
||||
$rows = $this->groupRemove((int)$parsedBody['action']['groupRemove']);
|
||||
if ($rows > 0) {
|
||||
$view->addFlashMessage($this->getLanguageService()->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:msg.group.deleted'));
|
||||
} else {
|
||||
$view->addFlashMessage($this->getLanguageService()->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:msg.group.delete.failed'), '', ContextualFeedbackSeverity::WARNING);
|
||||
}
|
||||
}
|
||||
return $this->renderListTasksView($view, $moduleData, $request);
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX endpoint for setup check modal content.
|
||||
*/
|
||||
public function setupCheckAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$view = $this->moduleTemplateFactory->create($request);
|
||||
$view->assign('dateFormat', [
|
||||
'day' => $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'] ?? 'd-m-y',
|
||||
'time' => $GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'] ?? 'H:i',
|
||||
]);
|
||||
$this->addSetupCheckInformation($view);
|
||||
return $view->renderResponse('CheckScreen');
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a task as deleted.
|
||||
*/
|
||||
private function deleteTask(ModuleTemplate $view, int $taskUid): void
|
||||
{
|
||||
$languageService = $this->getLanguageService();
|
||||
if ($taskUid <= 0) {
|
||||
throw new \RuntimeException('Expecting a valid task uid', 1641670374);
|
||||
}
|
||||
try {
|
||||
// Try to fetch the task and delete it
|
||||
$task = $this->taskRepository->findByUid($taskUid);
|
||||
if ($this->taskRepository->isTaskMarkedAsRunning($task)) {
|
||||
// If the task is currently running, it may not be deleted
|
||||
$this->addMessage($view, $languageService->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:msg.canNotDeleteRunningTask'), ContextualFeedbackSeverity::ERROR);
|
||||
} else {
|
||||
if ($this->taskRepository->remove($task)) {
|
||||
$this->addMessage($view, $languageService->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:msg.deleteSuccess'));
|
||||
} else {
|
||||
$this->addMessage($view, $languageService->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:msg.deleteError'));
|
||||
}
|
||||
}
|
||||
} catch (\UnexpectedValueException) {
|
||||
// The task could not be unserialized, simply update the database record setting it to deleted
|
||||
$result = $this->taskRepository->remove($taskUid);
|
||||
if ($result) {
|
||||
$this->addMessage($view, $languageService->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:msg.deleteSuccess'));
|
||||
} else {
|
||||
$this->addMessage($view, $languageService->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:msg.deleteError'), ContextualFeedbackSeverity::ERROR);
|
||||
}
|
||||
} catch (\OutOfBoundsException) {
|
||||
// The task was not found, for some reason
|
||||
$this->addMessage($view, sprintf($languageService->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:msg.taskNotFound'), $taskUid), ContextualFeedbackSeverity::ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the registered running executions from the task.
|
||||
* Note this doesn't actually stop the running script. It just unmarks execution.
|
||||
* @todo find a way to really kill the running task.
|
||||
*/
|
||||
private function stopTask(ModuleTemplate $view, int $taskUid): void
|
||||
{
|
||||
$languageService = $this->getLanguageService();
|
||||
if ($taskUid <= 0) {
|
||||
throw new \RuntimeException('Expecting a valid task uid', 1641670375);
|
||||
}
|
||||
try {
|
||||
// Try to fetch the task and stop it
|
||||
$task = $this->taskRepository->findByUid($taskUid);
|
||||
if ($this->taskRepository->isTaskMarkedAsRunning($task)) {
|
||||
// If the task is indeed currently running, clear marked executions
|
||||
$result = $this->taskRepository->removeAllRegisteredExecutionsForTask($task);
|
||||
if ($result) {
|
||||
$this->addMessage($view, $languageService->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:msg.stopSuccess'));
|
||||
} else {
|
||||
$this->addMessage($view, $languageService->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:msg.stopError'), ContextualFeedbackSeverity::ERROR);
|
||||
}
|
||||
} else {
|
||||
// The task is not running, nothing to unmark
|
||||
$this->addMessage($view, $languageService->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:msg.maynotStopNonRunningTask'), ContextualFeedbackSeverity::WARNING);
|
||||
}
|
||||
} catch (\OutOfBoundsException $e) {
|
||||
$this->addMessage($view, sprintf($languageService->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:msg.taskNotFound'), $taskUid), ContextualFeedbackSeverity::ERROR);
|
||||
} catch (\UnexpectedValueException $e) {
|
||||
$this->addMessage($view, sprintf($languageService->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:msg.stopTaskFailed'), $taskUid, $e->getMessage()), ContextualFeedbackSeverity::ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the disabled state of a task and register for next execution if a task is of type "single execution".
|
||||
*/
|
||||
private function toggleDisabledFlag(ModuleTemplate $view, int $taskUid): void
|
||||
{
|
||||
$languageService = $this->getLanguageService();
|
||||
if ($taskUid <= 0) {
|
||||
throw new \RuntimeException('Expecting a valid task uid to toggle disabled state', 1641670373);
|
||||
}
|
||||
try {
|
||||
$task = $this->taskRepository->findByUid($taskUid);
|
||||
// Toggle the task state and add a flash message
|
||||
$taskName = $this->taskService->getHumanReadableTaskName($task);
|
||||
$isTaskDisabled = $task->isDisabled();
|
||||
// If a disabled single task is enabled again, register it for a single execution at next scheduler run.
|
||||
if ($isTaskDisabled && $task->getExecution()->isSingleRun()) {
|
||||
$task->setDisabled(false);
|
||||
$task->setRunOnNextCronJob(true);
|
||||
$execution = Execution::createSingleExecution($this->context->getAspect('date')->get('timestamp'));
|
||||
$task->setExecution($execution);
|
||||
$this->addMessage($view, sprintf($languageService->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:msg.taskEnabledAndQueuedForExecution'), $taskName, $taskUid));
|
||||
} elseif ($isTaskDisabled) {
|
||||
$task->setDisabled(false);
|
||||
$this->addMessage($view, sprintf($languageService->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:msg.taskEnabled'), $taskName, $taskUid));
|
||||
} else {
|
||||
$task->setDisabled(true);
|
||||
$this->addMessage($view, sprintf($languageService->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:msg.taskDisabled'), $taskName, $taskUid));
|
||||
}
|
||||
$this->taskRepository->updateExecution($task);
|
||||
} catch (\OutOfBoundsException) {
|
||||
$this->addMessage($view, sprintf($languageService->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:msg.taskNotFound'), $taskUid), ContextualFeedbackSeverity::ERROR);
|
||||
} catch (\UnexpectedValueException $e) {
|
||||
$this->addMessage($view, sprintf($languageService->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:msg.toggleDisableFailed'), $taskUid, $e->getMessage()), ContextualFeedbackSeverity::ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a list of tasks.
|
||||
*/
|
||||
private function executeTasks(ModuleTemplate $view, string $taskUids): void
|
||||
{
|
||||
$taskUids = GeneralUtility::intExplode(',', $taskUids, true);
|
||||
if (empty($taskUids)) {
|
||||
throw new \RuntimeException('Expecting a list of task uids to execute', 1641715832);
|
||||
}
|
||||
// Loop selected tasks and execute.
|
||||
$languageService = $this->getLanguageService();
|
||||
foreach ($taskUids as $uid) {
|
||||
try {
|
||||
$task = $this->taskRepository->findByUid($uid);
|
||||
$name = $this->taskService->getHumanReadableTaskName($task);
|
||||
// Try to execute it and report result
|
||||
$result = $this->scheduler->executeTask($task);
|
||||
if ($result) {
|
||||
$this->addMessage($view, sprintf($languageService->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:msg.executed'), $name, $uid));
|
||||
} else {
|
||||
$this->addMessage($view, sprintf($languageService->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:msg.notExecuted'), $name, $uid), ContextualFeedbackSeverity::ERROR);
|
||||
}
|
||||
$this->scheduler->recordLastRun('manual');
|
||||
} catch (\OutOfBoundsException $e) {
|
||||
$this->addMessage($view, sprintf($languageService->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:msg.taskNotFound'), $uid), ContextualFeedbackSeverity::ERROR);
|
||||
} catch (\Exception $e) {
|
||||
$this->addMessage($view, sprintf($languageService->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:msg.executionFailed'), $uid, $e->getMessage()), ContextualFeedbackSeverity::ERROR);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule selected tasks to be executed on next cron run
|
||||
*/
|
||||
private function scheduleCrons(ModuleTemplate $view, string $taskUids): void
|
||||
{
|
||||
$taskUids = GeneralUtility::intExplode(',', $taskUids, true);
|
||||
if (empty($taskUids)) {
|
||||
throw new \RuntimeException('Expecting a list of task uids to schedule', 1641715833);
|
||||
}
|
||||
// Loop selected tasks and register for next cron run.
|
||||
$languageService = $this->getLanguageService();
|
||||
foreach ($taskUids as $uid) {
|
||||
try {
|
||||
$task = $this->taskRepository->findByUid($uid);
|
||||
$name = $this->taskService->getHumanReadableTaskName($task);
|
||||
$task->setRunOnNextCronJob(true);
|
||||
if ($task->isDisabled()) {
|
||||
$task->setDisabled(false);
|
||||
$this->addMessage($view, sprintf($languageService->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:msg.taskEnabledAndQueuedForExecution'), $name, $uid));
|
||||
} else {
|
||||
$this->addMessage($view, sprintf($languageService->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:msg.taskQueuedForExecution'), $name, $uid));
|
||||
}
|
||||
$this->taskRepository->updateExecution($task);
|
||||
} catch (\OutOfBoundsException $e) {
|
||||
$this->addMessage($view, sprintf($languageService->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:msg.taskNotFound'), $uid), ContextualFeedbackSeverity::ERROR);
|
||||
} catch (\UnexpectedValueException $e) {
|
||||
$this->addMessage($view, sprintf($languageService->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:msg.schedulingFailed'), $uid, $e->getMessage()), ContextualFeedbackSeverity::ERROR);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Assemble a listing of scheduled tasks
|
||||
*/
|
||||
private function renderListTasksView(ModuleTemplate $view, ModuleData $moduleData, ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$languageService = $this->getLanguageService();
|
||||
$data = $this->taskRepository->getGroupedTasks();
|
||||
$hasAvailableTaskTypes = $this->taskService->getAllTaskTypes() !== [];
|
||||
|
||||
$groups = $data['taskGroupsWithTasks'] ?? [];
|
||||
$groups = array_map(
|
||||
static fn(int $key, array $group): array => array_merge($group, ['taskGroupCollapsed' => (bool)($moduleData->get('task-group-' . $key, false))]),
|
||||
array_keys($groups),
|
||||
$groups
|
||||
);
|
||||
|
||||
// Move "not assigned to group" to the end
|
||||
if (array_key_exists('uid', $groups[0] ?? []) && $groups[0]['uid'] === null) {
|
||||
$groupWithoutTaskGroup = $groups[0];
|
||||
unset($groups[0]);
|
||||
$groups[0] = $groupWithoutTaskGroup;
|
||||
}
|
||||
|
||||
$this->pageRenderer->loadJavaScriptModule('@typo3/scheduler/new-scheduler-task-wizard-button.js');
|
||||
$this->pageRenderer->loadJavaScriptModule('@typo3/scheduler/setup-check-button.js');
|
||||
|
||||
$view->assignMultiple([
|
||||
'groups' => $groups,
|
||||
'groupsWithoutTasks' => $this->getGroupsWithoutTasks($groups),
|
||||
'hasAvailableTaskTypes' => $hasAvailableTaskTypes,
|
||||
'errorClasses' => $data['errorClasses'],
|
||||
'returnUrl' => $this->uriBuilder->buildUriFromRoute('scheduler'),
|
||||
'errorClassesCollapsed' => (bool)($moduleData->get('task-group-missing', false)),
|
||||
]);
|
||||
$view->setTitle(
|
||||
$languageService->translate('title', 'scheduler.module'),
|
||||
$languageService->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:function.scheduler')
|
||||
);
|
||||
$view->makeDocHeaderModuleMenu();
|
||||
if ($hasAvailableTaskTypes) {
|
||||
$addTaskUrl = (string)$this->uriBuilder->buildUriFromRoute('ajax_new_scheduler_task_wizard', [
|
||||
'returnUrl' => $request->getAttribute('normalizedParams')->getRequestUri(),
|
||||
]);
|
||||
$view->assign('addTaskUrl', $addTaskUrl);
|
||||
$this->addDocHeaderAddTaskButton($view, $addTaskUrl);
|
||||
$this->addDocHeaderAddTaskGroupButton($view);
|
||||
$this->addDocHeaderSetupCheckButton($view);
|
||||
}
|
||||
$this->addDocHeaderShortcutButton($view, $languageService->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:function.scheduler'));
|
||||
return $view->renderResponse('ListTasks');
|
||||
}
|
||||
|
||||
private function addDocHeaderAddTaskButton(ModuleTemplate $moduleTemplate, string $url): void
|
||||
{
|
||||
$languageService = $this->getLanguageService();
|
||||
$addButton = $this->componentFactory->createGenericButton()
|
||||
->setTag('typo3-scheduler-new-task-wizard-button')
|
||||
->setIcon($this->iconFactory->getIcon('actions-plus', IconSize::SMALL))
|
||||
->setLabel($languageService->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:function.add'))
|
||||
->setShowLabelText(true)
|
||||
->setAttributes([
|
||||
'url' => $url,
|
||||
'subject' => $languageService->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:function.add'),
|
||||
]);
|
||||
$moduleTemplate->addButtonToButtonBar($addButton, ButtonBar::BUTTON_POSITION_LEFT, 2);
|
||||
}
|
||||
|
||||
private function addDocHeaderAddTaskGroupButton(ModuleTemplate $moduleTemplate): void
|
||||
{
|
||||
$languageService = $this->getLanguageService();
|
||||
$addButton = $this->componentFactory->createInputButton()
|
||||
->setTitle($languageService->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:function.group.add'))
|
||||
->setShowLabelText(true)
|
||||
->setIcon($this->iconFactory->getIcon('actions-plus', IconSize::SMALL))
|
||||
->setName('createSchedulerGroup')
|
||||
->setValue('1')
|
||||
->setClasses('t3js-create-group');
|
||||
$moduleTemplate->addButtonToButtonBar($addButton, ButtonBar::BUTTON_POSITION_LEFT, 3);
|
||||
}
|
||||
|
||||
private function addDocHeaderSetupCheckButton(ModuleTemplate $moduleTemplate): void
|
||||
{
|
||||
$languageService = $this->getLanguageService();
|
||||
$setupCheckButton = $this->componentFactory->createGenericButton()
|
||||
->setTag('typo3-scheduler-setup-check-button')
|
||||
->setIcon($this->iconFactory->getIcon('actions-window-cog', IconSize::SMALL))
|
||||
->setLabel($languageService->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:function.check'))
|
||||
->setShowLabelText(true)
|
||||
->setAttributes([
|
||||
'url' => (string)$this->uriBuilder->buildUriFromRoute('ajax_scheduler_setup_check'),
|
||||
'subject' => $languageService->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:function.check'),
|
||||
]);
|
||||
$moduleTemplate->addButtonToButtonBar($setupCheckButton, ButtonBar::BUTTON_POSITION_RIGHT, 0);
|
||||
}
|
||||
|
||||
private function addDocHeaderShortcutButton(ModuleTemplate $moduleTemplate, string $name): void
|
||||
{
|
||||
$moduleTemplate->getDocHeaderComponent()->setShortcutContext(
|
||||
'scheduler',
|
||||
$name
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a flash message to the flash message queue of this module.
|
||||
*/
|
||||
private function addMessage(ModuleTemplate $moduleTemplate, string $message, ContextualFeedbackSeverity $severity = ContextualFeedbackSeverity::OK): void
|
||||
{
|
||||
$moduleTemplate->addFlashMessage($message, '', $severity);
|
||||
}
|
||||
|
||||
private function getGroupsWithoutTasks(array $taskGroupsWithTasks): array
|
||||
{
|
||||
$uidGroupsWithTasks = array_filter(array_column($taskGroupsWithTasks, 'uid'));
|
||||
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('tx_scheduler_task_group');
|
||||
$queryBuilder->getRestrictions()->removeByType(HiddenRestriction::class);
|
||||
$resultEmptyGroups = $queryBuilder->select('*')
|
||||
->from('tx_scheduler_task_group')
|
||||
->orderBy('groupName');
|
||||
|
||||
// Only add where statement if we have taskGroups to consider.
|
||||
if (!empty($uidGroupsWithTasks)) {
|
||||
$resultEmptyGroups->where($queryBuilder->expr()->notIn('uid', $uidGroupsWithTasks));
|
||||
}
|
||||
|
||||
return $resultEmptyGroups->executeQuery()->fetchAllAssociative();
|
||||
}
|
||||
|
||||
private function groupRemove(int $groupId): int
|
||||
{
|
||||
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('tx_scheduler_task_group');
|
||||
return $queryBuilder->update('tx_scheduler_task_group')
|
||||
->where($queryBuilder->expr()->eq('uid', $groupId))
|
||||
->set('deleted', 1)
|
||||
->executeStatement();
|
||||
}
|
||||
|
||||
private function groupDisable(int $groupId, int $hidden): void
|
||||
{
|
||||
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('tx_scheduler_task_group');
|
||||
$queryBuilder->update('tx_scheduler_task_group')
|
||||
->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($groupId)))
|
||||
->set('hidden', $hidden)
|
||||
->executeStatement();
|
||||
}
|
||||
|
||||
private function addSetupCheckInformation(ViewInterface $view): void
|
||||
{
|
||||
$languageService = $this->getLanguageService();
|
||||
// Display information about the last automated run, as stored in the system registry.
|
||||
$lastRun = $this->registry->get('tx_scheduler', 'lastRun');
|
||||
$lastRunMessageLabel = 'msg.noLastRun';
|
||||
$lastRunMessageLabelArguments = [];
|
||||
$lastRunSeverity = ContextualFeedbackSeverity::WARNING->value;
|
||||
if (is_array($lastRun)) {
|
||||
if (empty($lastRun['end']) || empty($lastRun['start']) || empty($lastRun['type'])) {
|
||||
$lastRunMessageLabel = 'msg.incompleteLastRun';
|
||||
$lastRunSeverity = ContextualFeedbackSeverity::WARNING->value;
|
||||
} else {
|
||||
$lastRunMessageLabelArguments = [
|
||||
$lastRun['type'] === 'manual'
|
||||
? $languageService->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:label.manually')
|
||||
: $languageService->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:label.automatically'),
|
||||
date($GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'], $lastRun['start']),
|
||||
date($GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'], $lastRun['start']),
|
||||
date($GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'], $lastRun['end']),
|
||||
date($GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'], $lastRun['end']),
|
||||
];
|
||||
$lastRunMessageLabel = 'msg.lastRun';
|
||||
$lastRunSeverity = ContextualFeedbackSeverity::INFO->value;
|
||||
}
|
||||
}
|
||||
|
||||
// Information about cli script.
|
||||
$script = $this->determineExecutablePath();
|
||||
$isExecutableMessageLabel = 'msg.cliScriptNotExecutable';
|
||||
$isExecutableSeverity = ContextualFeedbackSeverity::ERROR->value;
|
||||
$composerMode = !$script && Environment::isComposerMode();
|
||||
if (!$composerMode) {
|
||||
// Check if CLI script is executable or not. Skip this check if running Windows since executable detection
|
||||
// is not reliable on this platform, the script will always appear as *not* executable.
|
||||
$isExecutable = Environment::isWindows() ? true : ($script && is_executable($script));
|
||||
if ($isExecutable) {
|
||||
$isExecutableMessageLabel = 'msg.cliScriptExecutable';
|
||||
$isExecutableSeverity = ContextualFeedbackSeverity::OK->value;
|
||||
}
|
||||
}
|
||||
|
||||
$view->assignMultiple([
|
||||
'composerMode' => $composerMode,
|
||||
'script' => $script,
|
||||
'lastRunMessageLabel' => $lastRunMessageLabel,
|
||||
'lastRunMessageLabelArguments' => $lastRunMessageLabelArguments,
|
||||
'lastRunSeverity' => $lastRunSeverity,
|
||||
'isExecutableMessageLabel' => $isExecutableMessageLabel,
|
||||
'isExecutableSeverity' => $isExecutableSeverity,
|
||||
]);
|
||||
}
|
||||
|
||||
private function determineExecutablePath(): ?string
|
||||
{
|
||||
if (!Environment::isComposerMode()) {
|
||||
return GeneralUtility::getFileAbsFileName('EXT:core/bin/typo3');
|
||||
}
|
||||
$composerJsonFile = getenv('TYPO3_PATH_COMPOSER_ROOT') . '/composer.json';
|
||||
if (!file_exists($composerJsonFile) || !($jsonContent = file_get_contents($composerJsonFile))) {
|
||||
return null;
|
||||
}
|
||||
$jsonConfig = @json_decode($jsonContent, true);
|
||||
if (empty($jsonConfig) || !is_array($jsonConfig)) {
|
||||
return null;
|
||||
}
|
||||
$vendorDir = trim($jsonConfig['config']['vendor-dir'] ?? 'vendor', '/');
|
||||
$binDir = trim($jsonConfig['config']['bin-dir'] ?? $vendorDir . '/bin', '/');
|
||||
return sprintf('%s/%s/typo3', getenv('TYPO3_PATH_COMPOSER_ROOT'), $binDir);
|
||||
}
|
||||
|
||||
private function getLanguageService(): LanguageService
|
||||
{
|
||||
return $GLOBALS['LANG'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
<?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\CronCommand;
|
||||
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* This class provides calculations for the cron command format.
|
||||
*
|
||||
* @internal not part of TYPO3 Public API
|
||||
*/
|
||||
class CronCommand
|
||||
{
|
||||
/**
|
||||
* Normalized sections of the cron command.
|
||||
* Comma separated lists of integers and the character '*' are allowed.
|
||||
*
|
||||
* field lower and upper bound
|
||||
* ----- --------------
|
||||
* minute 0-59
|
||||
* hour 0-23
|
||||
* day of month 1-31
|
||||
* month 1-12
|
||||
* day of week 1-7
|
||||
*/
|
||||
protected array $cronCommandSections;
|
||||
|
||||
/**
|
||||
* Timestamp of next execution date.
|
||||
* This value starts with 'now + 1 minute' if not set externally
|
||||
* by unit tests. After a call to calculateNextValue() it holds the timestamp of
|
||||
* the next execution date which matches the cron command restrictions.
|
||||
*/
|
||||
protected int $timestamp;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param string $cronCommand The cron command can hold any combination documented as valid
|
||||
* @param bool|int $timestamp Optional start time, used in unit tests
|
||||
*/
|
||||
public function __construct(string $cronCommand, bool|int $timestamp = false)
|
||||
{
|
||||
$cronCommand = NormalizeCommand::normalize($cronCommand);
|
||||
// Explode cron command to sections
|
||||
$this->cronCommandSections = GeneralUtility::trimExplode(' ', $cronCommand);
|
||||
// Initialize the values with the starting time
|
||||
// This takes care that the calculated time is always in the future
|
||||
if ($timestamp === false) {
|
||||
$timestamp = strtotime('+1 minute');
|
||||
} else {
|
||||
$timestamp += 60;
|
||||
}
|
||||
$this->timestamp = $this->roundTimestamp($timestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the date of the next execution.
|
||||
*
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function calculateNextValue(): void
|
||||
{
|
||||
$newTimestamp = $this->getTimestamp();
|
||||
// Calculate next minute and hour field
|
||||
$loopCount = 0;
|
||||
while (true) {
|
||||
$loopCount++;
|
||||
// If there was no match within two days, cron command is invalid.
|
||||
// The second day is needed to catch the summertime leap in some countries.
|
||||
if ($loopCount > 2880) {
|
||||
throw new \RuntimeException('Unable to determine next execution timestamp: Hour and minute combination is invalid.', 1291494126);
|
||||
}
|
||||
if ($this->minuteAndHourMatchesCronCommand($newTimestamp)) {
|
||||
break;
|
||||
}
|
||||
$newTimestamp += 60;
|
||||
}
|
||||
$loopCount = 0;
|
||||
while (true) {
|
||||
$loopCount++;
|
||||
// A date must match within the next 4 years, this high number makes
|
||||
// sure leap year cron command configuration are caught.
|
||||
// If the loop runs longer than that, the cron command is invalid.
|
||||
if ($loopCount > 1464) {
|
||||
throw new \RuntimeException('Unable to determine next execution timestamp: Day of month, month and day of week combination is invalid.', 1291501280);
|
||||
}
|
||||
if ($this->dayMatchesCronCommand($newTimestamp)) {
|
||||
break;
|
||||
}
|
||||
$newTimestamp += $this->numberOfSecondsInDay($newTimestamp);
|
||||
}
|
||||
$this->timestamp = $newTimestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get next timestamp
|
||||
*/
|
||||
public function getTimestamp(): int
|
||||
{
|
||||
return $this->timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cron command sections. Array of strings, each containing either
|
||||
* a list of comma separated integers or *
|
||||
*/
|
||||
public function getCronCommandSections(): array
|
||||
{
|
||||
return $this->cronCommandSections;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if current timestamp matches minute and hour cron command restriction.
|
||||
*/
|
||||
protected function minuteAndHourMatchesCronCommand(int $timestamp): bool
|
||||
{
|
||||
$minute = (int)date('i', $timestamp);
|
||||
$hour = (int)date('G', $timestamp);
|
||||
$commandMatch = false;
|
||||
if ($this->isInCommandList($this->cronCommandSections[0], $minute) && $this->isInCommandList($this->cronCommandSections[1], $hour)) {
|
||||
$commandMatch = true;
|
||||
}
|
||||
return $commandMatch;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if current timestamp matches day of month, month and day of week
|
||||
* cron command restriction
|
||||
*/
|
||||
protected function dayMatchesCronCommand(int $timestamp): bool
|
||||
{
|
||||
$dayOfMonth = (int)date('j', $timestamp);
|
||||
$month = (int)date('n', $timestamp);
|
||||
$dayOfWeek = (int)date('N', $timestamp);
|
||||
$isInDayOfMonth = $this->isInCommandList($this->cronCommandSections[2], $dayOfMonth);
|
||||
$isInMonth = $this->isInCommandList($this->cronCommandSections[3], $month);
|
||||
$isInDayOfWeek = $this->isInCommandList($this->cronCommandSections[4], $dayOfWeek);
|
||||
// Quote from vixiecron:
|
||||
// Note: The day of a command's execution can be specified by two fields — day of month, and day of week.
|
||||
// If both fields are restricted (i.e., aren't *), the command will be run when either field
|
||||
// matches the current time. For example, `30 4 1,15 * 5' would cause
|
||||
// a command to be run at 4:30 am on the 1st and 15th of each month, plus every Friday.
|
||||
$isDayOfMonthRestricted = (string)$this->cronCommandSections[2] !== '*';
|
||||
$isDayOfWeekRestricted = (string)$this->cronCommandSections[4] !== '*';
|
||||
if (!$isInMonth) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If both day-of-month and day-of-week are unrestricted, month match is enough.
|
||||
if (!$isDayOfMonthRestricted && !$isDayOfWeekRestricted) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Otherwise, at least one restriction must match.
|
||||
return ($isInDayOfMonth && $isDayOfMonthRestricted) || ($isInDayOfWeek && $isDayOfWeekRestricted);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if a given number validates a cron command section. The given cron
|
||||
* command must be a 'normalized' list with only comma separated integers or '*'
|
||||
*/
|
||||
protected function isInCommandList(string $commandExpression, int $numberToMatch): bool
|
||||
{
|
||||
if ($commandExpression === '*') {
|
||||
$inList = true;
|
||||
} else {
|
||||
$inList = GeneralUtility::inList($commandExpression, (string)$numberToMatch);
|
||||
}
|
||||
return $inList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to calculate number of seconds in a day.
|
||||
*
|
||||
* This is not always 86400 (60*60*24) and depends on the timezone:
|
||||
* Some countries like Germany have a summertime / wintertime switch,
|
||||
* on every last sunday in march clocks are forwarded by one hour (set from 2:00 to 3:00),
|
||||
* and on last sunday of october they are set back one hour (from 3:00 to 2:00).
|
||||
* This shortens and lengthens the length of a day by one hour.
|
||||
*/
|
||||
protected function numberOfSecondsInDay(int $timestamp): int
|
||||
{
|
||||
$now = mktime(0, 0, 0, (int)date('n', $timestamp), (int)date('j', $timestamp), (int)date('Y', $timestamp));
|
||||
// Make sure to be in next day, even if day has 25 hours
|
||||
$nextDay = $now + 60 * 60 * 25;
|
||||
$nextDay = mktime(0, 0, 0, (int)date('n', $nextDay), (int)date('j', $nextDay), (int)date('Y', $nextDay));
|
||||
return $nextDay - $now;
|
||||
}
|
||||
|
||||
/**
|
||||
* Round a timestamp down to full minute.
|
||||
*/
|
||||
protected function roundTimestamp(int $timestamp): int
|
||||
{
|
||||
return (int)(floor($timestamp / 60) * 60);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,336 @@
|
||||
<?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\CronCommand;
|
||||
|
||||
use TYPO3\CMS\Core\Utility\MathUtility;
|
||||
|
||||
/**
|
||||
* Validate and normalize a cron command.
|
||||
*
|
||||
* Special fields like three letter weekdays, ranges and steps are substituted
|
||||
* to a comma separated list of integers. Example:
|
||||
* '2-4 10-40/10 * mar * fri' will be normalized to '2,4 10,20,30,40 * * 3 1,2'
|
||||
*
|
||||
* @internal not part of TYPO3 Public API
|
||||
*/
|
||||
class NormalizeCommand
|
||||
{
|
||||
/**
|
||||
* Main API method: Get the cron command and normalize it.
|
||||
*
|
||||
* If no exception is thrown, the resulting cron command is validated
|
||||
* and consists of five whitespace separated fields, which are either
|
||||
* the letter '*' or a sorted, unique comma separated list of integers.
|
||||
*
|
||||
* @throws \InvalidArgumentException cron command is invalid or out of bounds
|
||||
*/
|
||||
public static function normalize(string $cronCommand): string
|
||||
{
|
||||
$cronCommand = trim($cronCommand);
|
||||
$cronCommand = self::convertKeywordsToCronCommand($cronCommand);
|
||||
return self::normalizeFields($cronCommand);
|
||||
}
|
||||
|
||||
/**
|
||||
* Accept special cron command keywords and convert to standard cron syntax.
|
||||
* Allowed keywords: @yearly, @annually, @monthly, @weekly, @daily, @midnight, @hourly
|
||||
*/
|
||||
protected static function convertKeywordsToCronCommand(string $cronCommand): string
|
||||
{
|
||||
switch ($cronCommand) {
|
||||
case '@yearly':
|
||||
case '@annually':
|
||||
$cronCommand = '0 0 1 1 *';
|
||||
break;
|
||||
case '@monthly':
|
||||
$cronCommand = '0 0 1 * *';
|
||||
break;
|
||||
case '@weekly':
|
||||
$cronCommand = '0 0 * * 0';
|
||||
break;
|
||||
case '@daily':
|
||||
|
||||
case '@midnight':
|
||||
$cronCommand = '0 0 * * *';
|
||||
break;
|
||||
case '@hourly':
|
||||
$cronCommand = '0 * * * *';
|
||||
break;
|
||||
}
|
||||
return $cronCommand;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize cron command field to list of integers or *
|
||||
*/
|
||||
protected static function normalizeFields(string $cronCommand): string
|
||||
{
|
||||
$fieldArray = self::splitFields($cronCommand);
|
||||
$fieldArray[0] = self::normalizeIntegerField($fieldArray[0]);
|
||||
$fieldArray[1] = self::normalizeIntegerField($fieldArray[1], 0, 23);
|
||||
$fieldArray[2] = self::normalizeIntegerField($fieldArray[2], 1, 31);
|
||||
$fieldArray[3] = self::normalizeMonthAndWeekdayField($fieldArray[3], true);
|
||||
$fieldArray[4] = self::normalizeMonthAndWeekdayField($fieldArray[4], false);
|
||||
return implode(' ', $fieldArray);
|
||||
}
|
||||
|
||||
/**
|
||||
* Split a given cron command like '23 * * * *' to an array with five fields.
|
||||
*
|
||||
* @throws \InvalidArgumentException If splitted array does not contain five entries
|
||||
*/
|
||||
protected static function splitFields(string $cronCommand): array
|
||||
{
|
||||
$fields = explode(' ', $cronCommand);
|
||||
if (count($fields) !== 5) {
|
||||
throw new \InvalidArgumentException('Unable to split given cron command to five fields.', 1291227373);
|
||||
}
|
||||
return $fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize month field.
|
||||
*/
|
||||
protected static function normalizeMonthAndWeekdayField(string $expression, bool $isMonthField = true): string
|
||||
{
|
||||
if ((string)$expression === '*') {
|
||||
$fieldValues = '*';
|
||||
} else {
|
||||
// Fragment expression by , / and - and substitute three letter code of month and weekday to numbers
|
||||
$listOfCommaValues = explode(',', $expression);
|
||||
$fieldArray = [];
|
||||
foreach ($listOfCommaValues as $listElement) {
|
||||
if (str_contains($listElement, '/')) {
|
||||
[$left, $right] = explode('/', $listElement);
|
||||
if (str_contains($left, '-')) {
|
||||
[$leftBound, $rightBound] = explode('-', $left);
|
||||
$leftBound = self::normalizeMonthAndWeekday($leftBound, $isMonthField);
|
||||
$rightBound = self::normalizeMonthAndWeekday($rightBound, $isMonthField);
|
||||
$left = $leftBound . '-' . $rightBound;
|
||||
} else {
|
||||
if ((string)$left !== '*') {
|
||||
$left = self::normalizeMonthAndWeekday($left, $isMonthField);
|
||||
}
|
||||
}
|
||||
$fieldArray[] = $left . '/' . $right;
|
||||
} elseif (str_contains($listElement, '-')) {
|
||||
[$left, $right] = explode('-', $listElement);
|
||||
$left = self::normalizeMonthAndWeekday($left, $isMonthField);
|
||||
$right = self::normalizeMonthAndWeekday($right, $isMonthField);
|
||||
$fieldArray[] = $left . '-' . $right;
|
||||
} else {
|
||||
$fieldArray[] = self::normalizeMonthAndWeekday($listElement, $isMonthField);
|
||||
}
|
||||
}
|
||||
$fieldValues = implode(',', $fieldArray);
|
||||
}
|
||||
return $isMonthField ? self::normalizeIntegerField($fieldValues, 1, 12) : self::normalizeIntegerField($fieldValues, 1, 7);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize integer field.
|
||||
*
|
||||
* @throws \InvalidArgumentException If field is invalid or out of bounds
|
||||
*/
|
||||
protected static function normalizeIntegerField(string $expression, int $lowerBound = 0, int $upperBound = 59): string
|
||||
{
|
||||
if ($expression === '*') {
|
||||
$fieldValues = '*';
|
||||
} else {
|
||||
$listOfCommaValues = explode(',', $expression);
|
||||
$fieldArray = [];
|
||||
foreach ($listOfCommaValues as $listElement) {
|
||||
if (str_contains($listElement, '/')) {
|
||||
[$left, $right] = explode('/', $listElement);
|
||||
if ($left === '*') {
|
||||
$leftList = self::convertRangeToListOfValues($lowerBound . '-' . $upperBound);
|
||||
} else {
|
||||
$leftList = self::convertRangeToListOfValues($left);
|
||||
}
|
||||
$fieldArray[] = self::reduceListOfValuesByStepValue($leftList . '/' . $right);
|
||||
} elseif (str_contains($listElement, '-')) {
|
||||
$fieldArray[] = self::convertRangeToListOfValues($listElement);
|
||||
} elseif (MathUtility::canBeInterpretedAsInteger($listElement)) {
|
||||
$fieldArray[] = $listElement;
|
||||
} elseif (strlen($listElement) === 2 && $listElement[0] === '0') {
|
||||
$fieldArray[] = (int)$listElement;
|
||||
} else {
|
||||
throw new \InvalidArgumentException('Unable to normalize integer field.', 1291429389);
|
||||
}
|
||||
}
|
||||
$fieldValues = implode(',', $fieldArray);
|
||||
}
|
||||
if ($fieldValues !== '*') {
|
||||
$fieldList = explode(',', $fieldValues);
|
||||
sort($fieldList);
|
||||
$fieldList = array_unique($fieldList);
|
||||
if (current($fieldList) < $lowerBound) {
|
||||
throw new \InvalidArgumentException('Lowest element in list is smaller than allowed.', 1291470084);
|
||||
}
|
||||
if (end($fieldList) > $upperBound) {
|
||||
throw new \InvalidArgumentException('An element in the list is higher than allowed.', 1291470170);
|
||||
}
|
||||
$fieldValues = implode(',', $fieldList);
|
||||
}
|
||||
return $fieldValues;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a range of integers to a list: 4-6 results in a string '4,5,6'
|
||||
*
|
||||
* @param string $range integer-integer
|
||||
* @throws \InvalidArgumentException If range can not be converted to list
|
||||
*/
|
||||
protected static function convertRangeToListOfValues(string $range): string
|
||||
{
|
||||
if ($range === '') {
|
||||
throw new \InvalidArgumentException('Unable to convert range to list of values with empty string.', 1291234985);
|
||||
}
|
||||
$rangeArray = explode('-', $range);
|
||||
// Sanitize fields and cast to integer
|
||||
foreach ($rangeArray as $fieldNumber => $fieldValue) {
|
||||
if (!MathUtility::canBeInterpretedAsInteger($fieldValue)) {
|
||||
throw new \InvalidArgumentException('Unable to convert value to integer.', 1291237668);
|
||||
}
|
||||
$rangeArray[$fieldNumber] = (int)$fieldValue;
|
||||
}
|
||||
|
||||
$rangeArrayCount = count($rangeArray);
|
||||
if ($rangeArrayCount === 1) {
|
||||
$resultList = $rangeArray[0];
|
||||
} elseif ($rangeArrayCount === 2) {
|
||||
$left = $rangeArray[0];
|
||||
$right = $rangeArray[1];
|
||||
if ($left > $right) {
|
||||
throw new \InvalidArgumentException('Unable to convert range to list: Left integer must not be greater than right integer.', 1291237145);
|
||||
}
|
||||
$resultListArray = [];
|
||||
for ($i = $left; $i <= $right; $i++) {
|
||||
$resultListArray[] = $i;
|
||||
}
|
||||
$resultList = implode(',', $resultListArray);
|
||||
} else {
|
||||
throw new \InvalidArgumentException('Unable to convert range to list of values.', 1291234986);
|
||||
}
|
||||
return (string)$resultList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reduce a given list of values by step value.
|
||||
* Following a range with ``/<number>'' specifies skips of the number's value through the range.
|
||||
* 1-5/2 -> 1,3,5
|
||||
* 2-10/3 -> 2,5,8
|
||||
*
|
||||
* @return string comma-separated list of valid values
|
||||
* @throws \InvalidArgumentException if step value is invalid or if resulting list is empty
|
||||
*/
|
||||
protected static function reduceListOfValuesByStepValue(string $stepExpression): string
|
||||
{
|
||||
if ($stepExpression === '') {
|
||||
throw new \InvalidArgumentException('Unable to convert step values.', 1291234987);
|
||||
}
|
||||
$stepValuesAndStepArray = explode('/', $stepExpression);
|
||||
$stepValuesAndStepArrayCount = count($stepValuesAndStepArray);
|
||||
if ($stepValuesAndStepArrayCount > 2) {
|
||||
throw new \InvalidArgumentException('Unable to convert step values: Multiple slashes found.', 1291242168);
|
||||
}
|
||||
$left = $stepValuesAndStepArray[0];
|
||||
$right = $stepValuesAndStepArray[1] ?? '';
|
||||
if ($left === '') {
|
||||
throw new \InvalidArgumentException('Unable to convert step values: Left part of / is empty.', 1291414955);
|
||||
}
|
||||
if ($right === '') {
|
||||
throw new \InvalidArgumentException('Unable to convert step values: Right part of / is empty.', 1291414956);
|
||||
}
|
||||
if (!MathUtility::canBeInterpretedAsInteger($right)) {
|
||||
throw new \InvalidArgumentException('Unable to convert step values: Right part must be a single integer.', 1291414957);
|
||||
}
|
||||
$right = (int)$right;
|
||||
$leftArray = explode(',', $left);
|
||||
$validValues = [];
|
||||
$currentStep = $right;
|
||||
foreach ($leftArray as $leftValue) {
|
||||
if (!MathUtility::canBeInterpretedAsInteger($leftValue)) {
|
||||
throw new \InvalidArgumentException('Unable to convert step values: Left part must be a single integer or comma separated list of integers.', 1291414958);
|
||||
}
|
||||
if ($currentStep === 0) {
|
||||
$currentStep = $right;
|
||||
}
|
||||
if ($currentStep === $right) {
|
||||
$validValues[] = (int)$leftValue;
|
||||
}
|
||||
$currentStep--;
|
||||
}
|
||||
if (empty($validValues)) {
|
||||
throw new \InvalidArgumentException('Unable to convert step values: Result value list is empty.', 1291414959);
|
||||
}
|
||||
return implode(',', $validValues);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatcher method for normalizeMonth and normalizeWeekday
|
||||
*/
|
||||
protected static function normalizeMonthAndWeekday(string $expression, bool $isMonth = true): string
|
||||
{
|
||||
$expression = $isMonth ? self::normalizeMonth($expression) : self::normalizeWeekday($expression);
|
||||
return (string)$expression;
|
||||
}
|
||||
|
||||
/**
|
||||
* Accept a string representation or integer number of a month like
|
||||
* 'jan', 'February', 01, ... and convert to normalized integer value between 1 and 12
|
||||
*
|
||||
* @throws \InvalidArgumentException If month string can not be converted to integer
|
||||
*/
|
||||
protected static function normalizeMonth(string $month): int
|
||||
{
|
||||
$timestamp = strtotime('2010-' . $month . '-01');
|
||||
// timestamp must be >= 2010-01-01 and <= 2010-12-01
|
||||
if (!$timestamp || $timestamp < strtotime('2010-01-01') || $timestamp > strtotime('2010-12-01')) {
|
||||
throw new \InvalidArgumentException('Unable to convert given month name.', 1291083486);
|
||||
}
|
||||
return (int)date('n', $timestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Accept a string representation or integer number of a weekday like
|
||||
* 'mon', 'Friday', 3, ... and convert to normalized integer value between 1 and 7
|
||||
*
|
||||
* @throws \InvalidArgumentException If weekday string can not be converted
|
||||
*/
|
||||
protected static function normalizeWeekday(string $weekday): int
|
||||
{
|
||||
$normalizedWeekday = false;
|
||||
// 0 (sunday) -> 7
|
||||
if ($weekday === '0') {
|
||||
$weekday = 7;
|
||||
}
|
||||
if ($weekday >= 1 && $weekday <= 7) {
|
||||
$normalizedWeekday = (int)$weekday;
|
||||
}
|
||||
if (!$normalizedWeekday) {
|
||||
// Convert string representation like 'sun' to integer
|
||||
$timestamp = strtotime('next ' . $weekday, (int)mktime(0, 0, 0, 1, 1, 2010));
|
||||
if (!$timestamp || $timestamp < strtotime('2010-01-01') || $timestamp > strtotime('2010-01-08')) {
|
||||
throw new \InvalidArgumentException('Unable to convert given weekday name.', 1291163589);
|
||||
}
|
||||
$normalizedWeekday = (int)date('N', $timestamp);
|
||||
}
|
||||
return $normalizedWeekday;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,682 @@
|
||||
<?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\Domain\Repository;
|
||||
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
|
||||
use TYPO3\CMS\Backend\Utility\BackendUtility;
|
||||
use TYPO3\CMS\Core\Authentication\CommandLineUserAuthentication;
|
||||
use TYPO3\CMS\Core\Context\Context;
|
||||
use TYPO3\CMS\Core\Core\Bootstrap;
|
||||
use TYPO3\CMS\Core\Core\Environment;
|
||||
use TYPO3\CMS\Core\Database\Connection;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
|
||||
use TYPO3\CMS\Core\Database\Query\Restriction\HiddenRestriction;
|
||||
use TYPO3\CMS\Core\DataHandling\DataHandler;
|
||||
use TYPO3\CMS\Core\Schema\Capability\TcaSchemaCapability;
|
||||
use TYPO3\CMS\Core\Schema\Field\StaticSelectFieldType;
|
||||
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
|
||||
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Scheduler\Exception\InvalidTaskException;
|
||||
use TYPO3\CMS\Scheduler\ProgressProviderInterface;
|
||||
use TYPO3\CMS\Scheduler\Service\TaskService;
|
||||
use TYPO3\CMS\Scheduler\Task\AbstractTask;
|
||||
use TYPO3\CMS\Scheduler\Task\TaskSerializer;
|
||||
use TYPO3\CMS\Scheduler\Task\TaskStatus;
|
||||
use TYPO3\CMS\Scheduler\Validation\Validator\TaskValidator;
|
||||
|
||||
/**
|
||||
* Repository class to fetch tasks available in the systems ready to be executed
|
||||
*
|
||||
* @internal not part of public TYPO3 Core API
|
||||
*/
|
||||
#[Autoconfigure(public: true)]
|
||||
readonly class SchedulerTaskRepository
|
||||
{
|
||||
protected const TABLE_NAME = 'tx_scheduler_task';
|
||||
|
||||
public function __construct(
|
||||
protected TaskSerializer $taskSerializer,
|
||||
protected TaskService $taskService,
|
||||
protected TcaSchemaFactory $tcaSchemaFactory,
|
||||
protected Context $context,
|
||||
protected ConnectionPool $connectionPool,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Adds a task to the pool.
|
||||
*
|
||||
* @param AbstractTask $task The object representing the task to add
|
||||
* @return bool TRUE if the task was successfully added, FALSE otherwise
|
||||
*/
|
||||
public function add(AbstractTask $task): bool
|
||||
{
|
||||
$taskUid = $task->getTaskUid();
|
||||
if (!empty($taskUid)) {
|
||||
return false;
|
||||
}
|
||||
$fields = $this->taskService->getFieldsForRecord($task);
|
||||
$fields['pid'] = 0;
|
||||
$newId = uniqid('NEW');
|
||||
$dataHandler = GeneralUtility::makeInstance(DataHandler::class);
|
||||
$dataHandler->start([
|
||||
self::TABLE_NAME => [
|
||||
$newId => $fields,
|
||||
],
|
||||
], []);
|
||||
$dataHandler->process_datamap();
|
||||
$taskUid = (int)$dataHandler->substNEWwithIDs[$newId];
|
||||
if ($taskUid) {
|
||||
$task->setTaskUid($taskUid);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a task completely from the system.
|
||||
*
|
||||
* @param int|AbstractTask $task The object representing the task to delete
|
||||
* @return bool TRUE if the task was successfully deleted, FALSE otherwise
|
||||
*/
|
||||
public function remove(int|AbstractTask $task): bool
|
||||
{
|
||||
$taskUid = is_int($task) ? $task : $task->getTaskUid();
|
||||
if (empty($taskUid)) {
|
||||
return false;
|
||||
}
|
||||
$dataHandler = GeneralUtility::makeInstance(DataHandler::class);
|
||||
$dataHandler->start([], [
|
||||
self::TABLE_NAME => [
|
||||
$taskUid => [
|
||||
'delete' => 1,
|
||||
],
|
||||
],
|
||||
]);
|
||||
$dataHandler->process_cmdmap();
|
||||
return $dataHandler->errorLog === [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a task in the pool.
|
||||
*/
|
||||
public function update(AbstractTask $task, ?array $fields = null): bool
|
||||
{
|
||||
$taskUid = $task->getTaskUid();
|
||||
if (empty($taskUid)) {
|
||||
return false;
|
||||
}
|
||||
$fields = $fields ?? $this->taskService->getFieldsForRecord($task);
|
||||
$dataHandler = GeneralUtility::makeInstance(DataHandler::class);
|
||||
$dataHandler->start([
|
||||
self::TABLE_NAME => [
|
||||
$taskUid => $fields,
|
||||
],
|
||||
], []);
|
||||
$dataHandler->process_datamap();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a task in the pool but only the execution information.
|
||||
*/
|
||||
public function updateExecution(AbstractTask $task, bool $forceDisablingTask = false): void
|
||||
{
|
||||
$taskUid = $task->getTaskUid();
|
||||
if (empty($taskUid)) {
|
||||
return;
|
||||
}
|
||||
$fields = $this->taskService->getFieldsForRecord($task);
|
||||
$fields = [
|
||||
'nextexecution' => $fields['nextexecution'],
|
||||
'disable' => $forceDisablingTask ? true : $fields['disable'],
|
||||
'execution_details' => $fields['execution_details'],
|
||||
];
|
||||
$backendUser = $GLOBALS['BE_USER'] ?? null;
|
||||
if ($backendUser === null && Environment::isCli()) {
|
||||
/** @var CommandLineUserAuthentication $backendUser */
|
||||
$backendUser = Bootstrap::initializeBackendUser(CommandLineUserAuthentication::class);
|
||||
$backendUser->authenticate();
|
||||
}
|
||||
$dataHandler = GeneralUtility::makeInstance(DataHandler::class);
|
||||
// We don't want every execution information logged
|
||||
$dataHandler->enableLogging = false;
|
||||
$dataHandler->start([
|
||||
self::TABLE_NAME => [
|
||||
$taskUid => $fields,
|
||||
],
|
||||
], [], $backendUser);
|
||||
$dataHandler->process_datamap();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches a task object from the db with the given $uid. The object representing
|
||||
* the next due task is returned.
|
||||
* If there are no tasks due, the method throws an exception.
|
||||
*
|
||||
* @param int $uid Primary key of a task
|
||||
* @throws \OutOfBoundsException
|
||||
* @throws \UnexpectedValueException
|
||||
*/
|
||||
public function findByUid(int $uid): AbstractTask
|
||||
{
|
||||
$row = BackendUtility::getRecord(self::TABLE_NAME, $uid);
|
||||
if (empty($row)) {
|
||||
// Although an uid was passed, no task with given was found
|
||||
throw new \OutOfBoundsException('No task with id ' . $uid . ' found', 1422044826);
|
||||
}
|
||||
|
||||
return $this->createValidTaskObjectOrDisableTask($row);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the DB record for a given task UID.
|
||||
*
|
||||
* @param int $uid Primary key of the task to get
|
||||
* @return array|null Database record for the task
|
||||
* @see findByUid()
|
||||
*/
|
||||
public function findRecordByUid(int $uid): ?array
|
||||
{
|
||||
$row = BackendUtility::getRecord(self::TABLE_NAME, $uid);
|
||||
if (empty($row)) {
|
||||
return null;
|
||||
}
|
||||
return $row;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch and unserialize a task object from the db. Returns the object representing the
|
||||
* next due task is returned. If there are no due tasks, the method throws an exception.
|
||||
*
|
||||
* @throws \UnexpectedValueException
|
||||
*/
|
||||
public function findNextExecutableTask(): ?AbstractTask
|
||||
{
|
||||
// If no uid is given, take any non-disabled task that has a next execution time in the past
|
||||
$queryBuilder = $this->connectionPool->getQueryBuilderForTable(self::TABLE_NAME);
|
||||
$queryBuilder->select(
|
||||
't.*'
|
||||
)
|
||||
->from(self::TABLE_NAME, 't')
|
||||
->setMaxResults(1);
|
||||
// Define where clause
|
||||
$queryBuilder->getRestrictions()->removeAll();
|
||||
$queryBuilder->leftJoin(
|
||||
't',
|
||||
'tx_scheduler_task_group',
|
||||
'g',
|
||||
$queryBuilder->expr()->eq('g.uid', $queryBuilder->expr()->castInt($queryBuilder->quoteIdentifier('t.task_group')))
|
||||
);
|
||||
$queryBuilder->where(
|
||||
$queryBuilder->expr()->eq('t.disable', $queryBuilder->createNamedParameter(0, Connection::PARAM_INT)),
|
||||
$queryBuilder->expr()->neq(
|
||||
't.nextexecution',
|
||||
$queryBuilder->createNamedParameter(0, Connection::PARAM_INT)
|
||||
),
|
||||
$queryBuilder->expr()->lte(
|
||||
't.nextexecution',
|
||||
$queryBuilder->createNamedParameter($GLOBALS['EXEC_TIME'], Connection::PARAM_INT)
|
||||
),
|
||||
$queryBuilder->expr()->or(
|
||||
$queryBuilder->expr()->eq('g.hidden', $queryBuilder->createNamedParameter(0, Connection::PARAM_INT)),
|
||||
$queryBuilder->expr()->isNull('g.hidden')
|
||||
),
|
||||
$queryBuilder->expr()->eq('t.deleted', $queryBuilder->createNamedParameter(0, Connection::PARAM_INT))
|
||||
);
|
||||
$queryBuilder->orderBy('t.priority', 'DESC')->addOrderBy('t.nextexecution', 'ASC');
|
||||
|
||||
$row = $queryBuilder->executeQuery()->fetchAssociative();
|
||||
if (empty($row)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->createValidTaskObjectOrDisableTask($row);
|
||||
}
|
||||
|
||||
/**
|
||||
* @todo This will get split up into errored classes
|
||||
*/
|
||||
public function getGroupedTasks(): array
|
||||
{
|
||||
// Get all registered tasks
|
||||
$queryBuilder = $this->connectionPool->getQueryBuilderForTable(self::TABLE_NAME);
|
||||
$queryBuilder->getRestrictions()->removeAll();
|
||||
$result = $queryBuilder->select('t.*')
|
||||
->addSelect(
|
||||
'g.groupName AS taskGroupName',
|
||||
'g.description AS taskGroupDescription',
|
||||
'g.uid AS taskGroupId',
|
||||
'g.color AS taskGroupColor',
|
||||
'g.deleted AS isTaskGroupDeleted',
|
||||
'g.hidden AS isTaskGroupHidden',
|
||||
)
|
||||
->from(self::TABLE_NAME, 't')
|
||||
->leftJoin(
|
||||
't',
|
||||
'tx_scheduler_task_group',
|
||||
'g',
|
||||
$queryBuilder->expr()->eq('g.uid', $queryBuilder->expr()->castInt($queryBuilder->quoteIdentifier('t.task_group')))
|
||||
)
|
||||
->where(
|
||||
$queryBuilder->expr()->eq('t.deleted', 0)
|
||||
)
|
||||
->orderBy('g.sorting')
|
||||
->executeQuery();
|
||||
|
||||
$taskGroupsWithTasks = [];
|
||||
$errorClasses = [];
|
||||
while ($row = $result->fetchAssociative()) {
|
||||
$taskData = [
|
||||
'uid' => (int)$row['uid'],
|
||||
'lastExecutionTime' => (int)$row['lastexecution_time'],
|
||||
'lastExecutionContext' => $row['lastexecution_context'],
|
||||
'errorMessage' => '',
|
||||
'description' => $row['description'],
|
||||
];
|
||||
|
||||
try {
|
||||
$taskObject = $this->taskSerializer->deserialize($row);
|
||||
} catch (InvalidTaskException $e) {
|
||||
$taskData['errorMessage'] = $e->getMessage();
|
||||
$taskData['taskType'] = $row['tasktype'] ?: $this->taskSerializer->extractClassName($row['serialized_task_object']);
|
||||
$errorClasses[] = $taskData;
|
||||
continue;
|
||||
}
|
||||
|
||||
$taskData['taskType'] = $taskObject->getTaskType();
|
||||
|
||||
if (!$this->isValidTaskObject($taskObject)) {
|
||||
$taskData['errorMessage'] = 'The task "' . $taskObject->getTaskType() . ' is not a valid task';
|
||||
$errorClasses[] = $taskData;
|
||||
continue;
|
||||
}
|
||||
|
||||
$taskInformation = $this->taskService->getTaskDetailsFromTask($taskObject);
|
||||
if ($taskInformation === null) {
|
||||
$taskData['errorMessage'] = 'The task ' . $taskObject->getTaskType() . ' is not a registered task';
|
||||
$errorClasses[] = $taskData;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($taskObject instanceof ProgressProviderInterface) {
|
||||
$taskData['progress'] = round((float)$taskObject->getProgress(), 2);
|
||||
}
|
||||
$taskData['fullTitle'] = $taskInformation['fullTitle'];
|
||||
$taskData['additionalInformation'] = $taskObject->getAdditionalInformation();
|
||||
$taskData['disabled'] = (bool)$row['disable'];
|
||||
$taskData['isRunning'] = !empty($row['serialized_executions']);
|
||||
$taskData['nextExecution'] = (int)$row['nextexecution'];
|
||||
$taskData['runningType'] = 'single';
|
||||
$taskData['frequency'] = '';
|
||||
if ($taskObject->getExecution()->isRecurring()) {
|
||||
$taskData['runningType'] = 'recurring';
|
||||
$taskData['frequency'] = $taskObject->getExecution()->getCronCmd() ?: $taskObject->getExecution()->getInterval();
|
||||
}
|
||||
$taskData['multiple'] = (bool)$taskObject->getExecution()->isParallelExecutionAllowed();
|
||||
$taskData['priority'] = (int)$row['priority'];
|
||||
$taskData['priorityLabel'] = $this->resolvePriorityLabel((int)$row['priority']);
|
||||
$taskData['lastExecutionFailure'] = false;
|
||||
if (!empty($row['lastexecution_failure'])) {
|
||||
$taskData['lastExecutionFailure'] = true;
|
||||
// only scalars are serialized in \TYPO3\CMS\Scheduler\Scheduler::executeTask
|
||||
$exceptionArray = @unserialize($row['lastexecution_failure'], ['allowed_classes' => false]);
|
||||
$taskData['lastExecutionFailureCode'] = '';
|
||||
$taskData['lastExecutionFailureMessage'] = '';
|
||||
if (is_array($exceptionArray)) {
|
||||
$taskData['lastExecutionFailureCode'] = $exceptionArray['code'];
|
||||
$taskData['lastExecutionFailureMessage'] = $exceptionArray['message'];
|
||||
}
|
||||
}
|
||||
|
||||
$taskData['statuses'] = $this->buildTaskStatuses($taskData, (bool)$row['isTaskGroupHidden']);
|
||||
|
||||
// If a group is deleted or no group is set it needs to go into "not assigned groups"
|
||||
$groupIndex = $row['isTaskGroupDeleted'] === 1 || $row['isTaskGroupDeleted'] === null ? 0 : (int)$row['task_group'];
|
||||
if (!isset($taskGroupsWithTasks[$groupIndex])) {
|
||||
$taskGroupsWithTasks[$groupIndex] = [
|
||||
'uid' => $row['taskGroupId'],
|
||||
'groupName' => $row['taskGroupName'],
|
||||
'description' => $row['taskGroupDescription'],
|
||||
'color' => $row['taskGroupColor'],
|
||||
'hidden' => $row['isTaskGroupHidden'],
|
||||
'tasks' => [],
|
||||
];
|
||||
}
|
||||
$taskGroupsWithTasks[$groupIndex]['tasks'][] = $taskData;
|
||||
}
|
||||
|
||||
return [
|
||||
'taskGroupsWithTasks' => $taskGroupsWithTasks,
|
||||
'errorClasses' => $errorClasses,
|
||||
];
|
||||
}
|
||||
|
||||
protected function createValidTaskObjectOrDisableTask(array $row): AbstractTask
|
||||
{
|
||||
$isInvalidTask = false;
|
||||
$task = null;
|
||||
try {
|
||||
$task = $this->taskSerializer->deserialize($row);
|
||||
} catch (InvalidTaskException) {
|
||||
$isInvalidTask = true;
|
||||
}
|
||||
if ($isInvalidTask || !$this->isValidTaskObject($task)) {
|
||||
$fieldName = $this->tcaSchemaFactory
|
||||
->get(self::TABLE_NAME)
|
||||
->getCapability(TcaSchemaCapability::RestrictionDisabledField)
|
||||
->getFieldName();
|
||||
if ((bool)$row[$fieldName] !== true) {
|
||||
// Forcibly set the disabled flag to 1 in the database (if not already set), so that the
|
||||
// task does not come up again and again for execution. Execute a simple update statement
|
||||
// to avoid triggering any DH hook again, which would lead to an infinity loop.
|
||||
$this->connectionPool
|
||||
->getConnectionForTable(self::TABLE_NAME)
|
||||
->update(self::TABLE_NAME, [$fieldName => 1], ['uid' => (int)$row['uid']]);
|
||||
}
|
||||
// Throw an exception to raise the problem
|
||||
// @todo: This should most likely be changed to a specific exception.
|
||||
throw new \UnexpectedValueException('Could not unserialize task', 1255083671);
|
||||
}
|
||||
|
||||
// The task is valid, return it
|
||||
if ($task->getTaskGroup() === null) {
|
||||
// Fix invalid task_group=NULL settings in order to avoid exceptions when saving on PostgreSQL
|
||||
$task->setTaskGroup(0);
|
||||
}
|
||||
return $task;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch and unserialize task objects selected with some (SQL) condition
|
||||
*/
|
||||
public function findNextExecutableTaskForUid(int $uid): ?AbstractTask
|
||||
{
|
||||
$queryBuilder = $this->connectionPool
|
||||
->getQueryBuilderForTable(self::TABLE_NAME);
|
||||
$queryBuilder->getRestrictions()
|
||||
->removeAll()
|
||||
->add(GeneralUtility::makeInstance(DeletedRestriction::class))
|
||||
->add(GeneralUtility::makeInstance(HiddenRestriction::class));
|
||||
|
||||
$queryBuilder
|
||||
->select('*')
|
||||
->from(self::TABLE_NAME)
|
||||
->where(
|
||||
$queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($uid, Connection::PARAM_INT)),
|
||||
$queryBuilder->expr()->neq('nextexecution', $queryBuilder->createNamedParameter(0, Connection::PARAM_INT)),
|
||||
$queryBuilder->expr()->lte('nextexecution', $queryBuilder->createNamedParameter($GLOBALS['EXEC_TIME'], Connection::PARAM_INT)),
|
||||
);
|
||||
|
||||
$result = $queryBuilder->executeQuery();
|
||||
while ($row = $result->fetchAssociative()) {
|
||||
try {
|
||||
$task = $this->taskSerializer->deserialize($row);
|
||||
} catch (InvalidTaskException) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Add the task to the list only if it is valid
|
||||
if ($this->isValidTaskObject($task)) {
|
||||
return $task;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public function isTaskMarkedAsRunning(AbstractTask $task): bool
|
||||
{
|
||||
$row = BackendUtility::getRecord(self::TABLE_NAME, $task->getTaskUid());
|
||||
return !empty($row['serialized_executions'] ?? null);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method adds current execution to the execution list.
|
||||
* It also logs the execution time and mode
|
||||
*
|
||||
* The execution id is guaranteed to start from zero if the task has no
|
||||
* currently running execution at the time of id allocation.
|
||||
*
|
||||
* @return int Execution id
|
||||
*/
|
||||
public function addExecutionToTask(AbstractTask $task): int
|
||||
{
|
||||
while (true) {
|
||||
$row = BackendUtility::getRecord(self::TABLE_NAME, $task->getTaskUid());
|
||||
|
||||
if ($row === null) {
|
||||
throw new \InvalidArgumentException(
|
||||
'Given task must have a persistence record associated with it',
|
||||
1741257045
|
||||
);
|
||||
}
|
||||
|
||||
$previousExecutions = isset($row['serialized_executions'])
|
||||
? (string)$row['serialized_executions']
|
||||
: null;
|
||||
|
||||
$runningExecutions = $previousExecutions !== null
|
||||
&& $previousExecutions !== ''
|
||||
// serialized in \TYPO3\CMS\Scheduler\Domain\Repository\SchedulerTaskRepository::addExecutionToTask as `array<int, int>`
|
||||
? unserialize($previousExecutions, ['allowed_classes' => false])
|
||||
: [];
|
||||
|
||||
// Count the number of existing executions and use that number as a key
|
||||
// (we need to know that number, because it is returned at the end of the method)
|
||||
$numExecutions = count($runningExecutions);
|
||||
$runningExecutions[$numExecutions] = time();
|
||||
$updateCount = $this->connectionPool
|
||||
->getConnectionForTable(self::TABLE_NAME)
|
||||
->update(
|
||||
self::TABLE_NAME,
|
||||
[
|
||||
'serialized_executions' => serialize($runningExecutions),
|
||||
'lastexecution_time' => time(),
|
||||
// Define the context in which the script is running
|
||||
'lastexecution_context' => Environment::isCli() ? 'CLI' : 'BE',
|
||||
],
|
||||
[
|
||||
'uid' => $task->getTaskUid(),
|
||||
'serialized_executions' => $previousExecutions,
|
||||
],
|
||||
[
|
||||
'serialized_executions' => Connection::PARAM_LOB,
|
||||
]
|
||||
);
|
||||
|
||||
if ($updateCount === 1) {
|
||||
return $numExecutions;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a given execution from the list
|
||||
*
|
||||
* @param int $executionID Id of the execution to remove.
|
||||
* @param string|array|null $failureReason Details of an exception to signal a failed execution.
|
||||
*/
|
||||
public function removeExecutionOfTask(AbstractTask $task, int $executionID, array|string|null $failureReason = null): void
|
||||
{
|
||||
while ($row = BackendUtility::getRecord(self::TABLE_NAME, $task->getTaskUid())) {
|
||||
$previousExecutions = (string)($row['serialized_executions'] ?? '');
|
||||
if ($previousExecutions === '') {
|
||||
break;
|
||||
}
|
||||
// serialized in \TYPO3\CMS\Scheduler\Domain\Repository\SchedulerTaskRepository::addExecutionToTask as `array<int, int>`
|
||||
$runningExecutions = unserialize($previousExecutions, ['allowed_classes' => false]);
|
||||
// Remove the selected execution
|
||||
unset($runningExecutions[$executionID]);
|
||||
if (!empty($runningExecutions)) {
|
||||
// Re-serialize the updated executions list (if necessary)
|
||||
$runningExecutionsSerialized = serialize($runningExecutions);
|
||||
} else {
|
||||
$runningExecutionsSerialized = '';
|
||||
}
|
||||
if (is_array($failureReason)) {
|
||||
$failureReason = json_encode($failureReason);
|
||||
}
|
||||
// Save the updated executions list
|
||||
$fieldUpdates = [
|
||||
'serialized_executions' => $runningExecutionsSerialized,
|
||||
];
|
||||
if ($failureReason !== null) {
|
||||
$fieldUpdates['lastexecution_failure'] = (string)$failureReason;
|
||||
}
|
||||
$updateCount = $this->connectionPool
|
||||
->getConnectionForTable(self::TABLE_NAME)
|
||||
->update(
|
||||
self::TABLE_NAME,
|
||||
$fieldUpdates,
|
||||
[
|
||||
'uid' => $task->getTaskUid(),
|
||||
'serialized_executions' => $previousExecutions,
|
||||
],
|
||||
[
|
||||
'serialized_executions' => Connection::PARAM_LOB,
|
||||
]
|
||||
);
|
||||
if ($updateCount === 1) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all marked executions
|
||||
*
|
||||
* @return bool TRUE if the clearing succeeded, FALSE otherwise
|
||||
*/
|
||||
public function removeAllRegisteredExecutionsForTask(AbstractTask $task): bool
|
||||
{
|
||||
// Set the serialized executions field to empty
|
||||
$result = $this->connectionPool
|
||||
->getConnectionForTable(self::TABLE_NAME)
|
||||
->update(
|
||||
self::TABLE_NAME,
|
||||
['serialized_executions' => ''],
|
||||
['uid' => $task->getTaskUid()],
|
||||
['serialized_executions' => Connection::PARAM_LOB]
|
||||
);
|
||||
return (bool)$result;
|
||||
}
|
||||
|
||||
/**
|
||||
* See if there are any tasks configured at all.
|
||||
*/
|
||||
public function hasTasks(): bool
|
||||
{
|
||||
$queryBuilder = $this->connectionPool->getQueryBuilderForTable(self::TABLE_NAME);
|
||||
$queryBuilder->getRestrictions()
|
||||
->removeAll()
|
||||
->add(GeneralUtility::makeInstance(DeletedRestriction::class));
|
||||
$queryBuilder
|
||||
->count('*')
|
||||
->from(self::TABLE_NAME);
|
||||
return $queryBuilder->executeQuery()->fetchOne() > 0;
|
||||
}
|
||||
|
||||
protected function isValidTaskObject($task): bool
|
||||
{
|
||||
return (new TaskValidator())->isValid($task);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the status flags of a single task into an ordered list, shared as a single
|
||||
* source of truth by the backend module listing and the "scheduler:list" CLI command.
|
||||
*
|
||||
* @return list<TaskStatus>
|
||||
*/
|
||||
private function buildTaskStatuses(array $task, bool $groupHidden): array
|
||||
{
|
||||
$now = $this->context->getAspect('date')->get('timestamp');
|
||||
$statuses = [];
|
||||
|
||||
if ($task['isRunning']) {
|
||||
$statuses[] = new TaskStatus(
|
||||
type: 'running',
|
||||
severity: ContextualFeedbackSeverity::INFO,
|
||||
state: 'running',
|
||||
label: 'scheduler.messages:status.running',
|
||||
);
|
||||
}
|
||||
if ($task['nextExecution'] && $task['nextExecution'] < $now && !$groupHidden && !$task['disabled']) {
|
||||
$statuses[] = new TaskStatus(
|
||||
type: 'late',
|
||||
severity: ContextualFeedbackSeverity::WARNING,
|
||||
state: 'warning',
|
||||
label: 'scheduler.messages:status.late',
|
||||
);
|
||||
}
|
||||
if ($task['disabled'] && !$task['isRunning']) {
|
||||
$statuses[] = new TaskStatus(
|
||||
type: 'disabled',
|
||||
severity: ContextualFeedbackSeverity::NOTICE,
|
||||
state: 'disabled',
|
||||
label: 'scheduler.messages:status.disabled',
|
||||
);
|
||||
}
|
||||
if ($groupHidden && !$task['isRunning']) {
|
||||
$statuses[] = new TaskStatus(
|
||||
type: 'disabledByGroup',
|
||||
severity: ContextualFeedbackSeverity::NOTICE,
|
||||
state: 'disabled',
|
||||
label: 'scheduler.messages:status.disabledByGroup',
|
||||
);
|
||||
}
|
||||
if ($task['lastExecutionFailure'] ?? false) {
|
||||
if (($task['lastExecutionFailureMessage'] ?? '') !== '') {
|
||||
$statuses[] = new TaskStatus(
|
||||
type: 'failure',
|
||||
severity: ContextualFeedbackSeverity::ERROR,
|
||||
state: 'danger',
|
||||
label: 'scheduler.messages:status.failure',
|
||||
message: 'scheduler.messages:msg.executionFailureReport',
|
||||
messageArguments: [
|
||||
$task['lastExecutionFailureCode'],
|
||||
$task['lastExecutionFailureMessage'],
|
||||
],
|
||||
);
|
||||
} else {
|
||||
$statuses[] = new TaskStatus(
|
||||
type: 'failure',
|
||||
severity: ContextualFeedbackSeverity::ERROR,
|
||||
state: 'default',
|
||||
label: 'scheduler.messages:status.failure',
|
||||
message: 'scheduler.messages:msg.executionFailureDefault',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $statuses;
|
||||
}
|
||||
|
||||
private function resolvePriorityLabel(int $priority): string
|
||||
{
|
||||
$field = $this->tcaSchemaFactory->get(self::TABLE_NAME)->getField('priority');
|
||||
if ($field instanceof StaticSelectFieldType) {
|
||||
foreach ($field->getItems() as $item) {
|
||||
if ((int)$item->getValue() === $priority) {
|
||||
return $item->getLabel();
|
||||
}
|
||||
}
|
||||
}
|
||||
return (string)$priority;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?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\Event;
|
||||
|
||||
use TYPO3\CMS\Scheduler\Task\AbstractTask;
|
||||
|
||||
/**
|
||||
* Listeners to this event receive information about a completed scheduler task execution,
|
||||
* including its success state and any thrown exception.
|
||||
*/
|
||||
final readonly class AfterTaskExecutionEvent
|
||||
{
|
||||
public function __construct(
|
||||
private AbstractTask $task,
|
||||
private bool $success,
|
||||
private ?\Throwable $exception = null,
|
||||
) {}
|
||||
|
||||
public function getTask(): AbstractTask
|
||||
{
|
||||
return $this->task;
|
||||
}
|
||||
|
||||
public function isSuccess(): bool
|
||||
{
|
||||
return $this->success;
|
||||
}
|
||||
|
||||
public function getException(): ?\Throwable
|
||||
{
|
||||
return $this->exception;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?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\Event;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
/**
|
||||
* Listeners to this event are able to modify the scheduler task items for the new task wizard
|
||||
*/
|
||||
final class ModifyNewSchedulerTaskWizardItemsEvent
|
||||
{
|
||||
public function __construct(
|
||||
private array $wizardItems,
|
||||
private readonly ServerRequestInterface $request,
|
||||
) {}
|
||||
|
||||
public function getWizardItems(): array
|
||||
{
|
||||
return $this->wizardItems;
|
||||
}
|
||||
|
||||
public function setWizardItems(array $wizardItems): void
|
||||
{
|
||||
$this->wizardItems = $wizardItems;
|
||||
}
|
||||
|
||||
public function addWizardItem(string $key, array $wizardItem): void
|
||||
{
|
||||
$this->wizardItems[$key] = $wizardItem;
|
||||
}
|
||||
|
||||
public function removeWizardItem(string $key): void
|
||||
{
|
||||
unset($this->wizardItems[$key]);
|
||||
}
|
||||
|
||||
public function getRequest(): ServerRequestInterface
|
||||
{
|
||||
return $this->request;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
<?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\EventListener;
|
||||
|
||||
use TYPO3\CMS\Core\Attribute\AsEventListener;
|
||||
use TYPO3\CMS\Core\Configuration\Event\BeforeTcaOverridesEvent;
|
||||
use TYPO3\CMS\Core\Console\CommandRegistry;
|
||||
use TYPO3\CMS\Scheduler\Task\ExecuteSchedulableCommandTask;
|
||||
|
||||
final readonly class AddSchedulableCommandsAsNativeTaskTypes
|
||||
{
|
||||
public function __construct(
|
||||
private CommandRegistry $commandRegistry,
|
||||
) {}
|
||||
|
||||
#[AsEventListener]
|
||||
public function __invoke(BeforeTcaOverridesEvent $event): void
|
||||
{
|
||||
$tca = $event->getTca();
|
||||
foreach ($this->commandRegistry->getSchedulableCommandsConfiguration() as $commandIdentifier => $commandConfiguration) {
|
||||
if (($commandConfiguration['aliasFor'] ?? '') !== '') {
|
||||
// If an alias is set, we need to filter out the alias to prevent duplicate scheduler items.
|
||||
continue;
|
||||
}
|
||||
$tca['tx_scheduler_task']['columns']['tasktype']['config']['items'][] = [
|
||||
'label' => $commandConfiguration['name'],
|
||||
'description' => $commandConfiguration['description'],
|
||||
'value' => $commandIdentifier,
|
||||
'icon' => 'mimetypes-x-tx_scheduler_task_group',
|
||||
'group' => explode(':', $commandIdentifier)[0],
|
||||
];
|
||||
$tca['tx_scheduler_task']['types'][$commandIdentifier] = [
|
||||
'showitem' => '
|
||||
--div--;core.form.tabs:general,
|
||||
tasktype,
|
||||
task_group,
|
||||
description,
|
||||
parameters,
|
||||
--div--;core.form.tabs:timing,
|
||||
--palette--;;execution,
|
||||
--div--;core.form.tabs:access,
|
||||
disable,
|
||||
--div--;core.form.tabs:extended,
|
||||
',
|
||||
'columnsOverrides' => [
|
||||
'parameters' => [
|
||||
'label' => 'LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:label.schedulableCommand.command_configuration',
|
||||
'description' => 'LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:label.schedulableCommand.command_configuration.description',
|
||||
'config' => [
|
||||
'renderType' => 'schedulableCommandConfiguration',
|
||||
],
|
||||
],
|
||||
],
|
||||
'taskOptions' => [
|
||||
// @todo When introducing the AsRecordTypeHandler attribute we can get rid of this option again
|
||||
'className' => ExecuteSchedulableCommandTask::class,
|
||||
],
|
||||
];
|
||||
}
|
||||
$event->setTca($tca);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
<?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\EventListener;
|
||||
|
||||
use TYPO3\CMS\Backend\Routing\UriBuilder;
|
||||
use TYPO3\CMS\Backend\Template\Components\ComponentFactory;
|
||||
use TYPO3\CMS\Backend\Template\Components\ModifyButtonBarEvent;
|
||||
use TYPO3\CMS\Core\Attribute\AsEventListener;
|
||||
use TYPO3\CMS\Core\Imaging\IconFactory;
|
||||
use TYPO3\CMS\Core\Imaging\IconSize;
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
use TYPO3\CMS\Core\Page\PageRenderer;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Listener replaces the "New" button of FormEngine with the button to open the scheduler task wizard
|
||||
*/
|
||||
final readonly class ReplaceAddNewButtonToFormEngine
|
||||
{
|
||||
public function __construct(
|
||||
private IconFactory $iconFactory,
|
||||
private PageRenderer $pageRenderer,
|
||||
private UriBuilder $uriBuilder,
|
||||
private ComponentFactory $componentFactory,
|
||||
) {}
|
||||
|
||||
#[AsEventListener]
|
||||
public function __invoke(ModifyButtonBarEvent $event): void
|
||||
{
|
||||
$request = $event->getRequest();
|
||||
|
||||
if (($request->getAttribute('routing')?->getRoute()?->getOptions()['_identifier'] ?? '') !== 'record_edit') {
|
||||
return;
|
||||
}
|
||||
|
||||
$editConfig = $request->getQueryParams()['edit'] ?? null;
|
||||
if (!is_array($editConfig) || $editConfig === [] || count($editConfig) > 1 || key($editConfig) !== 'tx_scheduler_task') {
|
||||
return;
|
||||
}
|
||||
|
||||
$buttons = $event->getButtons();
|
||||
$leftButtons = $buttons['left'] ?? [];
|
||||
|
||||
$this->pageRenderer->loadJavaScriptModule('@typo3/scheduler/new-scheduler-task-wizard-button.js');
|
||||
|
||||
$addTaskUrl = (string)$this->uriBuilder->buildUriFromRoute('ajax_new_scheduler_task_wizard', [
|
||||
'returnUrl' => GeneralUtility::sanitizeLocalUrl($request->getQueryParams()['returnUrl'] ?? '', $request) ?: $request->getAttribute('normalizedParams')->getRequestUri(),
|
||||
]);
|
||||
|
||||
$languageService = $this->getLanguageService();
|
||||
$newButton = $this->componentFactory->createGenericButton()
|
||||
->setTag('typo3-scheduler-new-task-wizard-button')
|
||||
->setIcon($this->iconFactory->getIcon('actions-plus', IconSize::SMALL))
|
||||
->setLabel($languageService->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:function.add'))
|
||||
->setShowLabelText(true)
|
||||
->setAttributes([
|
||||
'url' => $addTaskUrl,
|
||||
'subject' => $languageService->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:function.add'),
|
||||
]);
|
||||
|
||||
// Find and replace t3js-editform-new button
|
||||
// By replacing the existing button we ensure to respect TSconfig and that user has necessary permissions
|
||||
foreach ($leftButtons as $groupIndex => $buttonGroup) {
|
||||
foreach ($buttonGroup as $buttonIndex => $button) {
|
||||
if (method_exists($button, 'getClasses') && str_contains($button->getClasses(), 't3js-editform-new')) {
|
||||
$leftButtons[$groupIndex][$buttonIndex] = $newButton;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$buttons['left'] = $leftButtons;
|
||||
$event->setButtons($buttons);
|
||||
}
|
||||
|
||||
private function getLanguageService(): LanguageService
|
||||
{
|
||||
return $GLOBALS['LANG'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?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;
|
||||
|
||||
use TYPO3\CMS\Core\Exception as CoreException;
|
||||
|
||||
/**
|
||||
* A generic scheduler exception
|
||||
*/
|
||||
class Exception extends CoreException {}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?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\Exception;
|
||||
|
||||
use TYPO3\CMS\Scheduler\Exception;
|
||||
|
||||
/**
|
||||
* Thrown if a submitted date can not be converted to timestamp using the backend module.
|
||||
*/
|
||||
class InvalidDateException extends Exception {}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?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\Exception;
|
||||
|
||||
use TYPO3\CMS\Scheduler\Exception;
|
||||
|
||||
/**
|
||||
* Thrown if a Task could not be successfully unserialized, the unserialized
|
||||
* Task is not an instance of AbstractTask or is not registered at all.
|
||||
*/
|
||||
class InvalidTaskException extends Exception {}
|
||||
@@ -0,0 +1,371 @@
|
||||
<?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\Scheduler;
|
||||
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Scheduler\CronCommand\CronCommand;
|
||||
|
||||
/**
|
||||
* This class manages the logic of a particular execution of a task
|
||||
* @internal
|
||||
*/
|
||||
class Execution
|
||||
{
|
||||
/**
|
||||
* Start date of a task (timestamp)
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $start;
|
||||
|
||||
/**
|
||||
* End date of a task (timestamp)
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $end;
|
||||
|
||||
/**
|
||||
* Interval between executions (in seconds)
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $interval;
|
||||
|
||||
/**
|
||||
* Flag for concurrent executions: TRUE if allowed, FALSE otherwise (default)
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $multiple = false;
|
||||
|
||||
/**
|
||||
* The cron command string of this task,
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $cronCmd;
|
||||
|
||||
/**
|
||||
* This flag is used to mark a new single execution
|
||||
* See explanations in method setIsNewSingleExecution()
|
||||
*
|
||||
* @var bool
|
||||
* @see \TYPO3\CMS\Scheduler\Execution::setIsNewSingleExecution()
|
||||
*/
|
||||
protected $isNewSingleExecution = false;
|
||||
|
||||
public static function createFromDetails(array $details): self
|
||||
{
|
||||
$obj = new self();
|
||||
$obj->setStart((int)($details['start'] ?? 0));
|
||||
$obj->setEnd((int)($details['end'] ?? 0));
|
||||
$obj->setInterval((int)($details['interval'] ?? 0));
|
||||
$obj->setMultiple((bool)($details['multiple'] ?? false));
|
||||
$obj->setCronCmd((string)($details['cronCmd'] ?? ''));
|
||||
$obj->setIsNewSingleExecution((bool)($details['isNewSingleExecution'] ?? false));
|
||||
return $obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a single execution of the task
|
||||
*
|
||||
* @param int $timestamp Timestamp of the next execution
|
||||
*/
|
||||
public static function createSingleExecution(int $timestamp): self
|
||||
{
|
||||
$obj = new self();
|
||||
$obj->setStart($timestamp);
|
||||
$obj->setInterval(0);
|
||||
$obj->setEnd(0);
|
||||
$obj->setCronCmd('');
|
||||
$obj->setMultiple(false);
|
||||
$obj->setIsNewSingleExecution(true);
|
||||
return $obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a recurring execution of the task
|
||||
*
|
||||
* @param int $start The first date/time when this execution should occur (timestamp)
|
||||
* @param int $interval Execution interval in seconds
|
||||
* @param int $end The last date/time when this execution should occur (timestamp)
|
||||
* @param bool $multiple Set to FALSE if multiple executions of this task are not permitted in parallel
|
||||
* @param string $cronCmd Used like in crontab (minute hour day month weekday)
|
||||
*/
|
||||
public static function createRecurringExecution(int $start, int $interval, int $end = 0, bool $multiple = false, string $cronCmd = ''): self
|
||||
{
|
||||
$obj = new self();
|
||||
// Set general values
|
||||
$obj->setStart($start);
|
||||
$obj->setEnd($end);
|
||||
$obj->setMultiple($multiple);
|
||||
if (empty($cronCmd)) {
|
||||
// Use interval
|
||||
$obj->setInterval($interval);
|
||||
$obj->setCronCmd('');
|
||||
} else {
|
||||
// Use cron syntax
|
||||
$obj->setInterval(0);
|
||||
$obj->setCronCmd($cronCmd);
|
||||
}
|
||||
return $obj;
|
||||
}
|
||||
|
||||
/**********************************
|
||||
* Setters and getters
|
||||
**********************************/
|
||||
/**
|
||||
* This method is used to set the start date
|
||||
*
|
||||
* @param int $start Start date (timestamp)
|
||||
*/
|
||||
public function setStart($start)
|
||||
{
|
||||
$this->start = (int)$start;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is used to get the start date
|
||||
*
|
||||
* @return int Start date (timestamp)
|
||||
*/
|
||||
public function getStart()
|
||||
{
|
||||
return (int)$this->start;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is used to set the end date
|
||||
*
|
||||
* @param int $end End date (timestamp)
|
||||
*/
|
||||
public function setEnd($end)
|
||||
{
|
||||
$this->end = (int)$end;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is used to get the end date
|
||||
*
|
||||
* @return int End date (timestamp)
|
||||
*/
|
||||
public function getEnd()
|
||||
{
|
||||
return (int)$this->end;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is used to set the interval
|
||||
*
|
||||
* @param int $interval Interval (in seconds)
|
||||
*/
|
||||
public function setInterval($interval)
|
||||
{
|
||||
$this->interval = (int)$interval;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is used to get the interval
|
||||
*
|
||||
* @return int Interval (in seconds)
|
||||
*/
|
||||
public function getInterval()
|
||||
{
|
||||
return (int)$this->interval;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is used to set the multiple execution flag
|
||||
*
|
||||
* @param bool $multiple TRUE if concurrent executions are allowed, FALSE otherwise
|
||||
*/
|
||||
public function setMultiple($multiple)
|
||||
{
|
||||
$this->multiple = (bool)$multiple;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is used to get the multiple execution flag
|
||||
*
|
||||
* @return bool TRUE if concurrent executions are allowed, FALSE otherwise
|
||||
*/
|
||||
public function isParallelExecutionAllowed(): bool
|
||||
{
|
||||
return (bool)$this->multiple;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the value of the cron command
|
||||
*
|
||||
* @param string $cmd Cron command, using cron-like syntax
|
||||
*/
|
||||
public function setCronCmd($cmd)
|
||||
{
|
||||
$this->cronCmd = $cmd;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value of the cron command
|
||||
*
|
||||
* @return string Cron command, using cron-like syntax
|
||||
*/
|
||||
public function getCronCmd()
|
||||
{
|
||||
return $this->cronCmd;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether this is a newly created single execution.
|
||||
* This is necessary for the following reason: if a new single-running task
|
||||
* is created and its start date is in the past (even for only a few seconds),
|
||||
* the next run time calculation (which happens upon saving) will disable
|
||||
* that task, because it was meant to run only once and is in the past.
|
||||
* Setting this flag to TRUE preserves this task for a single run.
|
||||
* Upon next execution, this flag is set to FALSE.
|
||||
*
|
||||
* @param bool $isNewSingleExecution Is newly created single execution?
|
||||
* @see \TYPO3\CMS\Scheduler\Execution::getNextExecution()
|
||||
*/
|
||||
public function setIsNewSingleExecution($isNewSingleExecution)
|
||||
{
|
||||
$this->isNewSingleExecution = (bool)$isNewSingleExecution;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get whether this is a newly created single execution
|
||||
*
|
||||
* @return bool Is newly created single execution?
|
||||
*/
|
||||
public function getIsNewSingleExecution()
|
||||
{
|
||||
return (bool)$this->isNewSingleExecution;
|
||||
}
|
||||
|
||||
/**********************************
|
||||
* Execution calculations and logic
|
||||
**********************************/
|
||||
/**
|
||||
* This method gets or calculates the next execution date
|
||||
*
|
||||
* @return int Timestamp of the next execution
|
||||
* @throws \OutOfBoundsException
|
||||
*/
|
||||
public function getNextExecution()
|
||||
{
|
||||
if ($this->getIsNewSingleExecution()) {
|
||||
$this->setIsNewSingleExecution(false);
|
||||
return $this->getStart();
|
||||
}
|
||||
if (!$this->isEnded()) {
|
||||
// If the schedule has not yet run out, find out the next date
|
||||
if (!$this->isStarted()) {
|
||||
// If the schedule hasn't started yet, next date is start date
|
||||
$date = $this->getStart();
|
||||
} else {
|
||||
// If the schedule has already started, calculate next date
|
||||
if ($this->cronCmd) {
|
||||
// If it uses cron-like syntax, calculate next date
|
||||
$date = $this->getNextCronExecution();
|
||||
} elseif ($this->getInterval() == 0) {
|
||||
// If not and there's no interval either, it's a singe execution: use start date
|
||||
$date = $this->getStart();
|
||||
} else {
|
||||
// Otherwise calculate date based on interval
|
||||
$now = time();
|
||||
$date = $now + $this->getInterval() - ($now - $this->getStart()) % $this->getInterval();
|
||||
}
|
||||
// If date is in the future, throw an exception
|
||||
if (!empty($this->getEnd()) && $date > $this->getEnd()) {
|
||||
throw new \OutOfBoundsException('Next execution date is past end date.', 1250715528);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// The event has ended, throw an exception
|
||||
throw new \OutOfBoundsException('Task is past end date.', 1250715544);
|
||||
}
|
||||
return $date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the next execution from a cron command
|
||||
*
|
||||
* @return int Next execution (timestamp)
|
||||
*/
|
||||
public function getNextCronExecution()
|
||||
{
|
||||
$cronCmd = GeneralUtility::makeInstance(CronCommand::class, $this->getCronCmd());
|
||||
$cronCmd->calculateNextValue();
|
||||
return (int)$cronCmd->getTimestamp();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the schedule for a task is started or not
|
||||
*
|
||||
* @return bool TRUE if the schedule is already active, FALSE otherwise
|
||||
*/
|
||||
public function isStarted()
|
||||
{
|
||||
return $this->getStart() < time();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the schedule for a task is passed or not
|
||||
*
|
||||
* @return bool TRUE if the schedule is not active anymore, FALSE otherwise
|
||||
*/
|
||||
public function isEnded()
|
||||
{
|
||||
if ($this->getEnd() === 0) {
|
||||
// If no end is defined, the schedule never ends
|
||||
$result = false;
|
||||
} else {
|
||||
// Otherwise check if end is in the past
|
||||
$result = $this->getEnd() < time();
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Guess recurring type from the existing information
|
||||
* If an interval or a cron command is defined, it's a recurring task
|
||||
*/
|
||||
public function isRecurring(): bool
|
||||
{
|
||||
return !empty($this->getInterval()) || !empty($this->getCronCmd());
|
||||
}
|
||||
|
||||
public function isSingleRun(): bool
|
||||
{
|
||||
return !$this->isRecurring();
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
// The type cast is necessary as long as the DB migration (upgrade wizard) exists,
|
||||
// Because this way, the serialization (from unserialize()) kicks in
|
||||
// and cleans the values right away.
|
||||
// @todo We can then strong-type-hint in TYPO3 v16.0.
|
||||
return [
|
||||
'start' => (int)$this->start,
|
||||
'end' => (int)$this->end,
|
||||
'interval' => (int)$this->interval,
|
||||
'multiple' => (bool)$this->multiple,
|
||||
'cronCmd' => (string)$this->cronCmd,
|
||||
'isNewSingleExecution' => (bool)$this->isNewSingleExecution,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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\Scheduler;
|
||||
|
||||
/**
|
||||
* Failed execution exception
|
||||
*/
|
||||
class FailedExecutionException extends \RuntimeException {}
|
||||
@@ -0,0 +1,85 @@
|
||||
<?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\Form\Element;
|
||||
|
||||
use TYPO3\CMS\Backend\Form\Element\AbstractFormElement;
|
||||
use TYPO3\CMS\Core\Resource\Index\ExtractorInterface;
|
||||
use TYPO3\CMS\Core\Resource\Index\ExtractorRegistry;
|
||||
|
||||
/**
|
||||
* Renders registered extractors
|
||||
*
|
||||
* This is rendered for config type=none, renderType=registeredExtractors
|
||||
*
|
||||
* @internal This is a specific hook implementation and is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
final class RegisteredExtractors extends AbstractFormElement
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ExtractorRegistry $extractorRegistry
|
||||
) {}
|
||||
|
||||
public function render(): array
|
||||
{
|
||||
$lang = $this->getLanguageService();
|
||||
$extractors = $this->extractorRegistry->getExtractors();
|
||||
|
||||
if ($extractors !== []) {
|
||||
$bullets = [];
|
||||
foreach ($extractors as $extractor) {
|
||||
$bullets[] = sprintf(
|
||||
'<li class="list-group-item" title="%s">%s%s</li>',
|
||||
get_class($extractor),
|
||||
sprintf(
|
||||
$lang->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:label.fileStorageExtraction.registeredExtractors.extractor'),
|
||||
$this->formatExtractorClassName($extractor),
|
||||
$extractor->getPriority()
|
||||
),
|
||||
$this->getBackendUser()->shallDisplayDebugInformation() ? (' <code>[' . get_class($extractor) . ']</code>') : ''
|
||||
);
|
||||
}
|
||||
$html = '
|
||||
<div class="form-description">' . htmlspecialchars($lang->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:label.fileStorageExtraction.registeredExtractors.with_extractors')) . '</div>
|
||||
<ul class="list-group mt-2">' . implode(LF, $bullets) . '</ul>
|
||||
';
|
||||
} else {
|
||||
$html = '<div class="alert alert-warning">' . htmlspecialchars($lang->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:label.fileStorageExtraction.registeredExtractors.without_extractors')) . '/div>';
|
||||
}
|
||||
|
||||
$resultArray['html'] = '
|
||||
<fieldset>
|
||||
<legend class="form-label t3js-formengine-label">
|
||||
' . htmlspecialchars($lang->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:label.fileStorageExtraction.registeredExtractors')) . '
|
||||
</legend>
|
||||
' . $html . '
|
||||
</fieldset>
|
||||
';
|
||||
|
||||
return $resultArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* Since the class name can be very long considering the namespace, only take the final
|
||||
* part for better readability. The FQN of the class will be displayed as tooltip.
|
||||
*/
|
||||
private function formatExtractorClassName(ExtractorInterface $extractor): string
|
||||
{
|
||||
$extractorParts = explode('\\', get_class($extractor));
|
||||
return (string)array_pop($extractorParts);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
<?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\Form\Element;
|
||||
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Exception\CommandNotFoundException;
|
||||
use Symfony\Component\Console\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\Console\Exception\InvalidOptionException;
|
||||
use Symfony\Component\Console\Exception\RuntimeException;
|
||||
use Symfony\Component\Console\Input\ArrayInput;
|
||||
use Symfony\Component\Console\Input\InputDefinition;
|
||||
use TYPO3\CMS\Backend\Form\Element\AbstractFormElement;
|
||||
use TYPO3\CMS\Core\Console\CommandRegistry;
|
||||
use TYPO3\CMS\Core\View\ViewFactoryData;
|
||||
use TYPO3\CMS\Core\View\ViewFactoryInterface;
|
||||
use TYPO3\CMS\Scheduler\Domain\Repository\SchedulerTaskRepository;
|
||||
use TYPO3\CMS\Scheduler\Exception\InvalidTaskException;
|
||||
use TYPO3\CMS\Scheduler\Service\TaskService;
|
||||
use TYPO3\CMS\Scheduler\Task\ExecuteSchedulableCommandTask;
|
||||
|
||||
/**
|
||||
* Creates an element and shows the available configuration (arguments and options) for a schedulable commands.
|
||||
*
|
||||
* @internal This is a specific hook implementation and is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
class SchedulableCommandConfigurationElement extends AbstractFormElement
|
||||
{
|
||||
public function __construct(
|
||||
protected readonly TaskService $taskService,
|
||||
protected readonly CommandRegistry $commandRegistry,
|
||||
protected readonly SchedulerTaskRepository $taskRepository,
|
||||
protected readonly ViewFactoryInterface $viewFactory,
|
||||
) {}
|
||||
|
||||
public function render(): array
|
||||
{
|
||||
$resultArray = $this->initializeResultArray();
|
||||
$selectedTaskType = $this->data['databaseRow']['tasktype'][0] ?? '';
|
||||
if ($selectedTaskType === '') {
|
||||
return $resultArray;
|
||||
}
|
||||
$parameterArray = $this->data['parameterArray'];
|
||||
$itemName = $parameterArray['itemFormElName'];
|
||||
|
||||
try {
|
||||
$taskObject = $this->taskRepository->findByUid((int)$this->data['databaseRow']['uid']);
|
||||
} catch (\OutOfBoundsException) {
|
||||
// This happens for new tasks when 'uid' is set to "0" because we have a Task Type from defVals
|
||||
try {
|
||||
$taskObject = $this->taskService->createNewTask($selectedTaskType);
|
||||
} catch (InvalidTaskException) {
|
||||
// Given task type is not registered - skip this element
|
||||
return $resultArray;
|
||||
}
|
||||
}
|
||||
|
||||
if ($taskObject instanceof ExecuteSchedulableCommandTask === false) {
|
||||
// Task is not an executable schedulable command task
|
||||
return $resultArray;
|
||||
}
|
||||
|
||||
try {
|
||||
$command = $this->commandRegistry->get($selectedTaskType);
|
||||
} catch (CommandNotFoundException) {
|
||||
// Command not found
|
||||
return $resultArray;
|
||||
}
|
||||
|
||||
$argumentFields = $this->getCommandArgumentFields($command->getDefinition(), $taskObject);
|
||||
$optionFields = $this->getCommandOptionFields($command->getDefinition(), $taskObject);
|
||||
|
||||
if ($argumentFields !== [] || $optionFields !== []) {
|
||||
|
||||
$fieldInformationResult = $this->renderFieldInformation();
|
||||
$fieldInformationHtml = $fieldInformationResult['html'];
|
||||
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
|
||||
|
||||
$html = [];
|
||||
$html[] = '<div class="formengine-field-item t3js-formengine-field-item">';
|
||||
$html[] = $fieldInformationHtml;
|
||||
$html[] = '<div class="form-wizards-wrap">';
|
||||
$html[] = $this->renderCommandConfiguration(array_merge($argumentFields, $optionFields), $selectedTaskType, $itemName);
|
||||
$html[] = '</div>';
|
||||
if ($this->data['command'] === 'edit') {
|
||||
$html[] = $this->getRunOnCliInfo($taskObject, $command);
|
||||
}
|
||||
$html[] = '</div>';
|
||||
|
||||
$resultArray['html'] = $this->wrapWithFieldsetAndLegend(implode(LF, $html));
|
||||
}
|
||||
|
||||
return $resultArray;
|
||||
}
|
||||
|
||||
protected function getCommandArgumentFields(InputDefinition $inputDefinition, ExecuteSchedulableCommandTask $task): array
|
||||
{
|
||||
$fields = [];
|
||||
$argumentValues = $task->getArguments();
|
||||
foreach ($inputDefinition->getArguments() as $argument) {
|
||||
$name = $argument->getName();
|
||||
$defaultValue = $argument->getDefault();
|
||||
$task->addDefaultValue($name, $defaultValue);
|
||||
$value = $argumentValues[$name] ?? $defaultValue;
|
||||
|
||||
if (is_array($value) && $argument->isArray()) {
|
||||
$value = implode(',', $value);
|
||||
}
|
||||
|
||||
$fields['arguments'][$name] = [
|
||||
'label' => 'Argument "' . $argument->getName() . '"',
|
||||
'description' => $argument->getDescription(),
|
||||
'value' => $value,
|
||||
'required' => $argument->isRequired(),
|
||||
];
|
||||
}
|
||||
|
||||
return $fields;
|
||||
}
|
||||
|
||||
protected function getCommandOptionFields(InputDefinition $inputDefinition, ExecuteSchedulableCommandTask $task): array
|
||||
{
|
||||
$fields = [];
|
||||
$enabledOptions = $task->getOptions();
|
||||
$optionValues = $task->getOptionValues();
|
||||
foreach ($inputDefinition->getOptions() as $option) {
|
||||
$name = $option->getName();
|
||||
$defaultValue = $option->getDefault();
|
||||
$task->addDefaultValue($name, $defaultValue);
|
||||
$enabled = $enabledOptions[$name] ?? false;
|
||||
$value = $optionValues[$name] ?? $defaultValue;
|
||||
|
||||
if (is_array($value) && $option->isArray()) {
|
||||
$value = implode(',', $value);
|
||||
}
|
||||
|
||||
$fields['options'][$name] = [
|
||||
'label' => 'Option "' . $option->getName() . '"',
|
||||
'description' => $option->getDescription(),
|
||||
'enabled' => $enabled,
|
||||
'value' => $value,
|
||||
'valueOption' => $option->isValueRequired() || $option->isValueOptional() || $option->isArray(),
|
||||
];
|
||||
}
|
||||
|
||||
return $fields;
|
||||
}
|
||||
|
||||
protected function getRunOnCliInfo(ExecuteSchedulableCommandTask $taskObject, Command $command): string
|
||||
{
|
||||
$options = [];
|
||||
foreach ($taskObject->getOptions() as $name => $enabled) {
|
||||
if ($enabled) {
|
||||
$value = $taskObject->getOptionValues()[$name] ?? null;
|
||||
$options['--' . $name] = ($value === true) ? '' : $value;
|
||||
}
|
||||
}
|
||||
|
||||
$parameters = array_merge($taskObject->getArguments(), $options);
|
||||
|
||||
try {
|
||||
$input = new ArrayInput($parameters, $command->getDefinition());
|
||||
$arguments = $input->__toString();
|
||||
$cliCommand = '<pre class="language-bash mt-2 mb-0"><code class="language-bash">' . $command->getName() . ' ' . $arguments . '</code></pre>';
|
||||
} catch (RuntimeException|InvalidArgumentException $e) {
|
||||
$cliCommand = '<div class="badge badge-warning mt-2">' . htmlspecialchars(sprintf($this->getLanguageService()->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:msg.errorParsingArguments'), $e->getMessage())) . '</div>';
|
||||
} catch (InvalidOptionException $e) {
|
||||
$cliCommand = '<div class="badge badge-warning mt-2">' . htmlspecialchars(sprintf($this->getLanguageService()->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:msg.errorParsingOptions'), $e->getMessage())) . '</div>';
|
||||
}
|
||||
|
||||
return '
|
||||
<div class="card mt-3 mb-0">
|
||||
<div class="card-header">
|
||||
<div class="card-header-body">
|
||||
<h2 class="card-title">' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:label.runOnCli')) . '</h2>
|
||||
' . $cliCommand . '
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
';
|
||||
}
|
||||
|
||||
protected function renderCommandConfiguration(array $fields, string $taskType, string $itemName): string
|
||||
{
|
||||
return $this->viewFactory->create(
|
||||
new ViewFactoryData(
|
||||
templateRootPaths: ['EXT:scheduler/Resources/Private/Templates'],
|
||||
partialRootPaths: ['EXT:scheduler/Resources/Private/Partials'],
|
||||
layoutRootPaths: ['EXT:scheduler/Resources/Private/Layouts'],
|
||||
request: $this->data['request'],
|
||||
format: 'html',
|
||||
)
|
||||
)->assignMultiple([
|
||||
'taskType' => $taskType,
|
||||
'fields' => $fields,
|
||||
'itemName' => $itemName,
|
||||
'renderDebug' => $this->getBackendUser()->shallDisplayDebugInformation(),
|
||||
])->render('CommandConfiguration');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
<?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\Form\Element;
|
||||
|
||||
use TYPO3\CMS\Backend\Form\Element\AbstractFormElement;
|
||||
use TYPO3\CMS\Core\Imaging\IconFactory;
|
||||
use TYPO3\CMS\Scheduler\Service\TaskService;
|
||||
|
||||
/**
|
||||
* @internal This is a specific hook implementation and is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
class TaskTypeInfoElement extends AbstractFormElement
|
||||
{
|
||||
public function __construct(
|
||||
private readonly TaskService $taskService,
|
||||
private readonly IconFactory $iconFactory,
|
||||
) {}
|
||||
|
||||
public function render(): array
|
||||
{
|
||||
$languageService = $this->getLanguageService();
|
||||
$resultArray = $this->initializeResultArray();
|
||||
$parameterArray = $this->data['parameterArray'];
|
||||
$selectedValue = '';
|
||||
if (!empty($parameterArray['itemFormElValue'])) {
|
||||
if (is_array($parameterArray['itemFormElValue'])) {
|
||||
$selectedValue = (string)$parameterArray['itemFormElValue'][0];
|
||||
} else {
|
||||
$selectedValue = (string)$parameterArray['itemFormElValue'];
|
||||
}
|
||||
}
|
||||
|
||||
$taskDetails = $this->taskService->getTaskDetailsFromTaskType($selectedValue);
|
||||
if ($taskDetails) {
|
||||
$resultArray['html'] = '
|
||||
<div class="card mb-0">
|
||||
<div class="card-header">
|
||||
<div class="card-icon">
|
||||
' . $this->iconFactory->getIcon(($taskDetails['icon'] ?? '') ?: 'mimetypes-x-tx_scheduler_task_group')->render() . '
|
||||
</div>
|
||||
<div class="card-header-body">
|
||||
<h2 class="card-title">' . htmlspecialchars($taskDetails['title']) . '</h2>
|
||||
<span class="card-subtitle">' . htmlspecialchars($taskDetails['description']) . '</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
';
|
||||
} else {
|
||||
$resultArray['html'] = '<div class="alert alert-warning">' . htmlspecialchars($languageService->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:msg.invalidTaskType')) . ': <code>' . htmlspecialchars($selectedValue) . '</code></div>';
|
||||
}
|
||||
|
||||
$resultArray['html'] .= '<input type="hidden" name="' . $parameterArray['itemFormElName'] . '" value="' . htmlspecialchars($selectedValue) . '" />';
|
||||
return $resultArray;
|
||||
}
|
||||
}
|
||||
@@ -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\Scheduler\Form\Element;
|
||||
|
||||
use TYPO3\CMS\Backend\Form\Element\AbstractFormElement;
|
||||
use TYPO3\CMS\Backend\Form\Element\CheckboxElement;
|
||||
use TYPO3\CMS\Backend\Form\Element\DatetimeElement;
|
||||
use TYPO3\CMS\Backend\Form\Element\InputTextElement;
|
||||
use TYPO3\CMS\Backend\Form\Element\RadioElement;
|
||||
use TYPO3\CMS\Core\Context\Context;
|
||||
use TYPO3\CMS\Core\Domain\DateTimeFactory;
|
||||
use TYPO3\CMS\Core\Page\JavaScriptModuleInstruction;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\View\ViewFactoryData;
|
||||
use TYPO3\CMS\Core\View\ViewFactoryInterface;
|
||||
use TYPO3\CMS\Scheduler\Execution;
|
||||
|
||||
/**
|
||||
* Creates an element to show a lot of details.
|
||||
*
|
||||
* This is rendered for config type=json, renderType=schedulerTimingOptions
|
||||
*
|
||||
* @internal This is a specific hook implementation and is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
class TimingOptionsElement extends AbstractFormElement
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ViewFactoryInterface $viewFactory,
|
||||
private readonly Context $context,
|
||||
) {}
|
||||
|
||||
public function render(): array
|
||||
{
|
||||
$languageService = $this->getLanguageService();
|
||||
$resultArray = $this->initializeResultArray();
|
||||
$parameterArray = $this->data['parameterArray'];
|
||||
$itemValue = $parameterArray['itemFormElValue'];
|
||||
$itemName = $parameterArray['itemFormElName'];
|
||||
|
||||
if (is_array($itemValue) && $itemValue !== []) {
|
||||
$executionDetails = Execution::createFromDetails($itemValue);
|
||||
} else {
|
||||
$executionDetails = new Execution();
|
||||
// Set the default value to "in 5 minutes"
|
||||
$executionDetails->setStart($this->context->getPropertyFromAspect('date', 'accessTime') + (5 * 60));
|
||||
}
|
||||
|
||||
$fieldsHtml = '';
|
||||
|
||||
$runningType = GeneralUtility::makeInstance(RadioElement::class);
|
||||
$runningType->data = $this->data;
|
||||
$runningType->data['containerFieldName'] = 'runningType';
|
||||
$runningType->data['parameterArray']['fieldConf']['label'] = htmlspecialchars($languageService->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:runningType'));
|
||||
$runningType->data['parameterArray']['itemFormElName'] .= '[runningType]';
|
||||
$runningType->data['parameterArray']['fieldConf']['config']['items'] = [
|
||||
['label' => $languageService->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:label.type.single'), 'value' => 1],
|
||||
['label' => $languageService->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:label.type.recurring'), 'value' => 2],
|
||||
];
|
||||
$runningType->data['parameterArray']['itemFormElValue'] = $executionDetails->isSingleRun() ? 1 : 2;
|
||||
$runningType->data['parameterArray']['fieldChangeFunc'] = [];
|
||||
$runningType->data['parameterArray']['fieldConf'] = array_replace_recursive($runningType->data['parameterArray']['fieldConf'], $parameterArray['fieldConf']['config']['overrideFieldTca']['runningType'] ?? []);
|
||||
$subFieldResult = $runningType->render();
|
||||
$resultArray['javaScriptModules'] = array_merge($resultArray['javaScriptModules'], $subFieldResult['javaScriptModules']);
|
||||
$fieldsHtml .= '<div class="form-group col-sm-6 t3js-timing-options-runningType">' . str_replace('"form-check"', '"form-check form-inline me-2"', $subFieldResult['html']) . '</div>';
|
||||
|
||||
$multiple = GeneralUtility::makeInstance(CheckboxElement::class);
|
||||
$multiple->data = $this->data;
|
||||
$multiple->data['containerFieldName'] = 'multiple';
|
||||
$multiple->data['parameterArray']['fieldConf']['label'] = htmlspecialchars($languageService->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:label.parallel.long'));
|
||||
$multiple->data['parameterArray']['itemFormElName'] .= '[multiple]';
|
||||
$multiple->data['parameterArray']['fieldConf']['config']['items'] = [];
|
||||
$multiple->data['parameterArray']['fieldChangeFunc'] = [];
|
||||
$multiple->data['parameterArray']['itemFormElValue'] = $executionDetails->isParallelExecutionAllowed();
|
||||
$multiple->data['parameterArray']['fieldConf'] = array_replace_recursive($multiple->data['parameterArray']['fieldConf'], $parameterArray['fieldConf']['config']['overrideFieldTca']['multiple'] ?? []);
|
||||
$subFieldResult = $multiple->render();
|
||||
$resultArray['javaScriptModules'] = array_merge($resultArray['javaScriptModules'], $subFieldResult['javaScriptModules']);
|
||||
$fieldsHtml .= '<div class="form-group col-sm-6 t3js-timing-options-parallel">' . $subFieldResult['html'] . '</div>';
|
||||
|
||||
$start = GeneralUtility::makeInstance(DatetimeElement::class);
|
||||
$start->data = $this->data;
|
||||
$start->data['containerFieldName'] = 'start';
|
||||
$start->data['parameterArray']['fieldConf']['label'] = htmlspecialchars($languageService->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:scheduledFrom'));
|
||||
$start->data['parameterArray']['itemFormElName'] .= '[start]';
|
||||
$start->data['parameterArray']['itemFormElValue'] = DateTimeFactory::createFromTimestamp($executionDetails->getStart() ?: $this->context->getPropertyFromAspect('date', 'timestamp'));
|
||||
$start->data['parameterArray']['fieldConf'] = array_replace_recursive($start->data['parameterArray']['fieldConf'], $parameterArray['fieldConf']['config']['overrideFieldTca']['start'] ?? []);
|
||||
$subFieldResult = $start->render();
|
||||
$resultArray['javaScriptModules'] = array_merge($resultArray['javaScriptModules'], $subFieldResult['javaScriptModules']);
|
||||
$fieldsHtml .= '<div class="form-group col-sm-6 t3js-timing-options-start">' . $subFieldResult['html'] . '</div>';
|
||||
|
||||
$end = GeneralUtility::makeInstance(DatetimeElement::class);
|
||||
$end->data = $this->data;
|
||||
$end->data['containerFieldName'] = 'end';
|
||||
$end->data['parameterArray']['fieldConf']['label'] = htmlspecialchars($languageService->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:scheduledUntil'));
|
||||
$end->data['parameterArray']['itemFormElName'] .= '[end]';
|
||||
$end->data['parameterArray']['itemFormElValue'] = $executionDetails->getEnd() ? DateTimeFactory::createFromTimestamp($executionDetails->getEnd()) : null;
|
||||
$end->data['parameterArray']['fieldConf'] = array_replace_recursive($end->data['parameterArray']['fieldConf'], $parameterArray['fieldConf']['config']['overrideFieldTca']['end'] ?? []);
|
||||
$subFieldResult = $end->render();
|
||||
$resultArray['javaScriptModules'] = array_merge($resultArray['javaScriptModules'], $subFieldResult['javaScriptModules']);
|
||||
$fieldsHtml .= '<div class="form-group col-sm-6 t3js-timing-options-end">' . $subFieldResult['html'] . '</div>';
|
||||
|
||||
$frequency = GeneralUtility::makeInstance(InputTextElement::class);
|
||||
$frequency->data = $this->data;
|
||||
$frequency->data['containerFieldName'] = 'frequency';
|
||||
$frequency->data['parameterArray']['fieldConf']['label'] = htmlspecialchars($languageService->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:label.frequency.long'));
|
||||
$frequency->data['parameterArray']['itemFormElName'] .= '[frequency]';
|
||||
$frequency->data['parameterArray']['itemFormElValue'] = $executionDetails->getCronCmd() ?: $executionDetails->getInterval();
|
||||
$frequency->data['parameterArray']['fieldChangeFunc'] = [];
|
||||
$frequency->data['parameterArray']['fieldConf']['config']['size'] = 40;
|
||||
$frequency->data['parameterArray']['fieldConf'] = array_replace_recursive($frequency->data['parameterArray']['fieldConf'], $parameterArray['fieldConf']['config']['overrideFieldTca']['frequency'] ?? []);
|
||||
$subFieldResult = $frequency->render();
|
||||
$resultArray['javaScriptModules'] = array_merge($resultArray['javaScriptModules'], $subFieldResult['javaScriptModules']);
|
||||
$fieldsHtml .= '<div class="form-group t3js-timing-options-frequency">' . $subFieldResult['html'] . '</div>';
|
||||
|
||||
$fieldInformationResult = $this->renderFieldInformation();
|
||||
$fieldInformationHtml = $fieldInformationResult['html'];
|
||||
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
|
||||
|
||||
$html = [];
|
||||
$html[] = '<typo3-formengine-element-timing-options class="formengine-field-item t3js-formengine-field-item" fieldPrefix="' . htmlspecialchars($itemName) . '">';
|
||||
$html[] = $fieldInformationHtml;
|
||||
$html[] = '<div class="form-control-wrap" style="max-width: ' . $this->formMaxWidth((int)($this->defaultInputWidth * 1.5)) . 'px">';
|
||||
$html[] = '<div class="form-wizards-wrap">';
|
||||
$html[] = '<div class="form-wizards-item-element">';
|
||||
$html[] = '<div class="row">' . $fieldsHtml . $this->renderServerTime() . '</div>';
|
||||
$html[] = '</div>';
|
||||
$html[] = '</div>';
|
||||
$html[] = '</div>';
|
||||
$html[] = '</typo3-formengine-element-timing-options>';
|
||||
|
||||
$resultArray['html'] = $this->wrapWithFieldsetAndLegend(implode(LF, $html));
|
||||
$resultArray['javaScriptModules'][] = JavaScriptModuleInstruction::create('@typo3/scheduler/form-engine/element/timing-options-element.js');
|
||||
|
||||
return $resultArray;
|
||||
}
|
||||
|
||||
protected function renderServerTime(): string
|
||||
{
|
||||
$view = $this->viewFactory->create(
|
||||
new ViewFactoryData(
|
||||
templateRootPaths: ['EXT:scheduler/Resources/Private/Templates'],
|
||||
partialRootPaths: ['EXT:scheduler/Resources/Private/Partials'],
|
||||
layoutRootPaths: ['EXT:scheduler/Resources/Private/Layouts'],
|
||||
request: $this->data['request'],
|
||||
format: 'html',
|
||||
)
|
||||
);
|
||||
$view->assignMultiple([
|
||||
'dateFormat' => [
|
||||
'day' => $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'] ?? 'd-m-y',
|
||||
'time' => $GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'] ?? 'H:i',
|
||||
],
|
||||
]);
|
||||
return $view->render('ServerTime');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<?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\Form\FieldInformation;
|
||||
|
||||
use TYPO3\CMS\Backend\Form\AbstractNode;
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Scheduler\Task\TableGarbageCollectionTask;
|
||||
|
||||
/**
|
||||
* Renders "expiresPeriod" information for the selected table, which is used if nothing is specified for this field manually.
|
||||
*
|
||||
* @internal This is a specific scheduler implementation and is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
class ExpirePeriodInformation extends AbstractNode
|
||||
{
|
||||
public function render(): array
|
||||
{
|
||||
$resultArray = $this->initializeResultArray();
|
||||
|
||||
if ($this->data['command'] !== 'edit'
|
||||
|| $this->data['tableName'] !== 'tx_scheduler_task'
|
||||
|| (int)($this->data['parameterArray']['itemFormElValue'] ?? 0) > 0
|
||||
) {
|
||||
return $resultArray;
|
||||
}
|
||||
|
||||
$refField = (string)($this->data['renderData']['fieldInformationOptions']['refField'] ?? '');
|
||||
if (($this->data['databaseRow'][$refField] ?? false) === false) {
|
||||
return $resultArray;
|
||||
}
|
||||
|
||||
$selectedTable = (string)(is_array($this->data['databaseRow'][$refField]) ? $this->data['databaseRow'][$refField][0] : $this->data['databaseRow'][$refField]);
|
||||
$tableConfiguration = GeneralUtility::makeInstance(TableGarbageCollectionTask::class)->getTableConfiguration()[$selectedTable] ?? [];
|
||||
if (!isset($tableConfiguration['expirePeriod'])) {
|
||||
return $resultArray;
|
||||
}
|
||||
|
||||
$resultArray['html'] = '
|
||||
<div class="badge badge-info mb-2">
|
||||
' . sprintf($this->getLanguageService()->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:msg.defaultExpirePeriod'), (int)$tableConfiguration['expirePeriod'], $selectedTable) . '
|
||||
</div>';
|
||||
|
||||
return $resultArray;
|
||||
}
|
||||
|
||||
protected function getLanguageService(): LanguageService
|
||||
{
|
||||
return $GLOBALS['LANG'];
|
||||
}
|
||||
}
|
||||
@@ -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'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
<?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\Migration;
|
||||
|
||||
use Doctrine\DBAL\ArrayParameterType;
|
||||
use Doctrine\DBAL\ParameterType;
|
||||
use TYPO3\CMS\Core\Attribute\UpgradeWizard;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
|
||||
use TYPO3\CMS\Core\Serializer\DenyListDeserializer;
|
||||
use TYPO3\CMS\Core\Upgrades\DatabaseUpdatedPrerequisite;
|
||||
use TYPO3\CMS\Core\Upgrades\UpgradeWizardInterface;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Scheduler\Service\TaskService;
|
||||
use TYPO3\CMS\Scheduler\Task\AbstractTask;
|
||||
use TYPO3\CMS\Scheduler\Task\ExecuteSchedulableCommandTask;
|
||||
use TYPO3\CMS\Scheduler\Task\TaskSerializer;
|
||||
|
||||
/**
|
||||
* @since 14.0
|
||||
* @internal This class is only meant to be used within EXT:scheduler and is not part of the TYPO3 Core API.
|
||||
*/
|
||||
#[UpgradeWizard('schedulerDatabaseStorageMigration')]
|
||||
class SchedulerDatabaseStorageMigration implements UpgradeWizardInterface
|
||||
{
|
||||
protected const TABLE_NAME = 'tx_scheduler_task';
|
||||
|
||||
public function __construct(private readonly DenyListDeserializer $deserializer) {}
|
||||
|
||||
public function getTitle(): string
|
||||
{
|
||||
return 'Migrate the contents of the tx_scheduler_task database table into a more structured form.';
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Each Scheduler Task was previously stored in a serialized object format in the database. This update wizard migrates records of this type to a JSON-formatted storage in the database. If this wizard does not disappear, it means there are tasks that failed to be migrated and may need manual inspection or re-creation. When this happens, inspect all tasks of tx_scheduler_task where the "tasktype" column is empty.';
|
||||
}
|
||||
|
||||
public function getPrerequisites(): array
|
||||
{
|
||||
return [
|
||||
DatabaseUpdatedPrerequisite::class,
|
||||
];
|
||||
}
|
||||
|
||||
public function updateNecessary(): bool
|
||||
{
|
||||
return $this->hasRecordsToUpdate();
|
||||
}
|
||||
|
||||
public function executeUpdate(): bool
|
||||
{
|
||||
$connection = $this->getConnectionPool()->getConnectionForTable(self::TABLE_NAME);
|
||||
$table = $this->getConnectionPool()->getConnectionForTable(self::TABLE_NAME)->createSchemaManager()->introspectSchema()->getTable(self::TABLE_NAME);
|
||||
$taskSerializer = GeneralUtility::makeInstance(TaskSerializer::class);
|
||||
$taskService = GeneralUtility::makeInstance(TaskService::class);
|
||||
$hasFailures = false;
|
||||
foreach ($this->getRecordsToUpdate() as $record) {
|
||||
try {
|
||||
// Base migration was already done, but not the migration to additional fields, so we'll do this now
|
||||
if (!empty($record['tasktype'])) {
|
||||
$taskObject = $taskSerializer->deserialize($record);
|
||||
} else {
|
||||
// unserialize() will only give a E_NOTICE and false result, not throw an error. Silence this
|
||||
// (for tests) and operate on the "false". If future PHP promotes this to an exception, the Throwable
|
||||
// catch will kick in.
|
||||
$taskObject = $this->deserializer->deserialize($record['serialized_task_object']);
|
||||
}
|
||||
if ($taskObject instanceof AbstractTask) {
|
||||
$fieldsToUpdate = [
|
||||
'tasktype' => $taskObject->getTaskType(),
|
||||
'execution_details' => $taskObject->getExecution()?->toArray(),
|
||||
];
|
||||
$taskDetails = $taskService->getTaskDetailsFromTask($taskObject);
|
||||
$taskParameters = $taskObject->getTaskParameters();
|
||||
if (($taskDetails['isNativeTask'] ?? false) && $taskDetails['className'] !== ExecuteSchedulableCommandTask::class) {
|
||||
// map native types to real fields, and do not use the parameters' value. Only
|
||||
// exception to this are console commands, which are native types but use the
|
||||
// parameters as well, because they have dynamic configuration (arguments, options).
|
||||
if (is_array($taskDetails['additionalFields'] ?? false) && $taskDetails['additionalFields'] !== []) {
|
||||
foreach ($taskDetails['additionalFields'] as $additionalFieldName) {
|
||||
$fieldsToUpdate[$additionalFieldName] = $taskParameters[$additionalFieldName] ?? null;
|
||||
}
|
||||
}
|
||||
$fieldsToUpdate['parameters'] = null;
|
||||
} else {
|
||||
$fieldsToUpdate['parameters'] = $taskParameters;
|
||||
}
|
||||
$connection->update(
|
||||
self::TABLE_NAME,
|
||||
array_filter($fieldsToUpdate, static fn($column) => $table->hasColumn($column), ARRAY_FILTER_USE_KEY),
|
||||
['uid' => (int)$record['uid']]
|
||||
);
|
||||
} elseif ($taskObject instanceof \__PHP_Incomplete_Class) {
|
||||
$objectVars = get_mangled_object_vars($taskObject);
|
||||
$properties = [];
|
||||
$executionDetails = null;
|
||||
$taskType = null;
|
||||
foreach ($objectVars as $key => $value) {
|
||||
$key = trim($key);
|
||||
$key = trim($key, "*\0");
|
||||
$key = trim($key);
|
||||
if ($key === '__PHP_Incomplete_Class_Name') {
|
||||
$taskType = $value;
|
||||
} else {
|
||||
switch ($key) {
|
||||
case '__PHP_Incomplete_Class_Name':
|
||||
$taskType = $value;
|
||||
break;
|
||||
case 'execution':
|
||||
$executionDetails = $value;
|
||||
break;
|
||||
case 'progress':
|
||||
case 'scheduler':
|
||||
case 'taskUid':
|
||||
case 'disabled':
|
||||
case 'runOnNextCronJob': // mapped to "task_group" in the database
|
||||
case 'executionTime': // mapped to "next_execution" in the database
|
||||
case 'taskGroup': // mapped to "task_group" in the database
|
||||
case 'description':
|
||||
break;
|
||||
default:
|
||||
if (is_scalar($value) || is_null($value)) {
|
||||
$properties[$key] = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$connection->update(
|
||||
self::TABLE_NAME,
|
||||
[
|
||||
'tasktype' => $taskType,
|
||||
'parameters' => $properties,
|
||||
'execution_details' => $executionDetails?->toArray(),
|
||||
],
|
||||
['uid' => (int)$record['uid']]
|
||||
);
|
||||
} else {
|
||||
// This happens if unserialize() failed (gracefully).
|
||||
// Wizard shall not be marked as completed and show up again to let people know.
|
||||
$hasFailures = true;
|
||||
}
|
||||
} catch (\Throwable) {
|
||||
// Mark wizard as failed so the upgrade wizard will show up again, and people know there is a problem.
|
||||
$hasFailures = true;
|
||||
}
|
||||
}
|
||||
|
||||
return !$hasFailures;
|
||||
}
|
||||
|
||||
protected function hasRecordsToUpdate(): bool
|
||||
{
|
||||
// Check if table exists
|
||||
if (!$this->getConnectionPool()->getConnectionForTable(self::TABLE_NAME)->createSchemaManager()->tableExists(self::TABLE_NAME)) {
|
||||
return false;
|
||||
}
|
||||
return (bool)$this->getPreparedQueryBuilder()->count('uid')->executeQuery()->fetchOne();
|
||||
}
|
||||
|
||||
protected function getRecordsToUpdate(): array
|
||||
{
|
||||
return $this->getPreparedQueryBuilder()->select('*')->executeQuery()->fetchAllAssociative();
|
||||
}
|
||||
|
||||
protected function getPreparedQueryBuilder(): QueryBuilder
|
||||
{
|
||||
$nativeTaskTypesWithAdditionalFields = $this->getAllNativeTaskTypesWithAdditionalFields();
|
||||
|
||||
$queryBuilder = $this->getConnectionPool()->getQueryBuilderForTable(self::TABLE_NAME);
|
||||
// This is done by intention, so the upgrade wizard continues to work even if we introduce further TCA details for tx_scheduler_task
|
||||
$queryBuilder->getRestrictions()->removeAll();
|
||||
$queryBuilder
|
||||
->from(self::TABLE_NAME)
|
||||
->where(
|
||||
$queryBuilder->expr()->eq('deleted', $queryBuilder->createNamedParameter(0, ParameterType::INTEGER)),
|
||||
$queryBuilder->expr()->or(
|
||||
// Find all where the task type is empty (legacy serialized storage)
|
||||
// OR where we have a native task type, that contains additional fields we can migrate
|
||||
$queryBuilder->expr()->or(
|
||||
$queryBuilder->expr()->eq(
|
||||
'tasktype',
|
||||
$queryBuilder->createNamedParameter('')
|
||||
),
|
||||
$queryBuilder->expr()->isNull('tasktype')
|
||||
),
|
||||
$queryBuilder->expr()->and(
|
||||
$queryBuilder->expr()->in(
|
||||
'tasktype',
|
||||
$queryBuilder->createNamedParameter(array_keys($nativeTaskTypesWithAdditionalFields), ArrayParameterType::STRING)
|
||||
),
|
||||
$queryBuilder->expr()->isNotNull('parameters'),
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
return $queryBuilder;
|
||||
}
|
||||
|
||||
protected function getAllNativeTaskTypesWithAdditionalFields(): array
|
||||
{
|
||||
$taskService = GeneralUtility::makeInstance(TaskService::class);
|
||||
$allTaskInformation = $taskService->getAllTaskTypes();
|
||||
$nativeTaskTypesWithAdditionalFields = [];
|
||||
foreach ($allTaskInformation as $taskType => $taskInformation) {
|
||||
if (($taskInformation['isNativeTask'] ?? false) && $taskInformation['className'] !== ExecuteSchedulableCommandTask::class) {
|
||||
// Native tasks can define "additionalFields". However, console commands, which are
|
||||
// native tasks as well, do not define real fields but use the "parameters" feature.
|
||||
$nativeTaskTypesWithAdditionalFields[$taskType] = $taskInformation['additionalFields'] ?? [];
|
||||
}
|
||||
}
|
||||
return $nativeTaskTypesWithAdditionalFields;
|
||||
}
|
||||
|
||||
protected function getConnectionPool(): ConnectionPool
|
||||
{
|
||||
return GeneralUtility::makeInstance(ConnectionPool::class);
|
||||
}
|
||||
}
|
||||
@@ -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\Scheduler;
|
||||
|
||||
/**
|
||||
* Interface for tasks who can provide their progress
|
||||
*/
|
||||
interface ProgressProviderInterface
|
||||
{
|
||||
/**
|
||||
* Gets the progress of a task.
|
||||
*
|
||||
* @return float Progress of the task as a two decimal precision float. f.e. 44.87
|
||||
*/
|
||||
public function getProgress();
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
<?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\Scheduler;
|
||||
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use TYPO3\CMS\Core\Configuration\ExtensionConfiguration;
|
||||
use TYPO3\CMS\Core\Database\Connection;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\Registry;
|
||||
use TYPO3\CMS\Core\SingletonInterface;
|
||||
use TYPO3\CMS\Scheduler\Domain\Repository\SchedulerTaskRepository;
|
||||
use TYPO3\CMS\Scheduler\Event\AfterTaskExecutionEvent;
|
||||
use TYPO3\CMS\Scheduler\Exception\InvalidTaskException;
|
||||
use TYPO3\CMS\Scheduler\Task\AbstractTask;
|
||||
use TYPO3\CMS\Scheduler\Task\TaskSerializer;
|
||||
|
||||
/**
|
||||
* TYPO3 Scheduler. This class handles scheduling and execution of tasks.
|
||||
*/
|
||||
class Scheduler implements SingletonInterface
|
||||
{
|
||||
/**
|
||||
* @var array $extConf Settings from the extension manager
|
||||
*/
|
||||
public array $extConf = [];
|
||||
|
||||
/**
|
||||
* Constructor, makes sure all derived client classes are included
|
||||
*/
|
||||
public function __construct(
|
||||
protected readonly LoggerInterface $logger,
|
||||
protected readonly TaskSerializer $taskSerializer,
|
||||
protected readonly SchedulerTaskRepository $schedulerTaskRepository,
|
||||
protected readonly EventDispatcherInterface $eventDispatcher,
|
||||
protected readonly Registry $registry,
|
||||
protected readonly ConnectionPool $connectionPool,
|
||||
ExtensionConfiguration $extensionConfiguration,
|
||||
) {
|
||||
// Get configuration from the extension manager
|
||||
$this->extConf = $extensionConfiguration->get('scheduler');
|
||||
if (empty($this->extConf['maxLifetime'])) {
|
||||
$this->extConf['maxLifetime'] = 1440;
|
||||
}
|
||||
// Clean up the serialized execution arrays
|
||||
$this->cleanExecutionArrays();
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans the execution lists of the scheduled tasks, executions older than 24h are removed
|
||||
* @todo find a way to actually kill the job
|
||||
*/
|
||||
protected function cleanExecutionArrays()
|
||||
{
|
||||
$tstamp = $GLOBALS['EXEC_TIME'];
|
||||
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('tx_scheduler_task');
|
||||
|
||||
// Select all tasks with executions
|
||||
// NOTE: this cleanup is done for disabled tasks too,
|
||||
// to avoid leaving old executions lying around
|
||||
$result = $queryBuilder->select('*')
|
||||
->from('tx_scheduler_task')
|
||||
->where(
|
||||
$queryBuilder->expr()->neq(
|
||||
'serialized_executions',
|
||||
$queryBuilder->createNamedParameter('')
|
||||
),
|
||||
$queryBuilder->expr()->eq('deleted', $queryBuilder->createNamedParameter(0, Connection::PARAM_INT))
|
||||
)
|
||||
->executeQuery();
|
||||
$maxDuration = $this->extConf['maxLifetime'] * 60;
|
||||
while ($row = $result->fetchAssociative()) {
|
||||
$executions = [];
|
||||
// serialized in \TYPO3\CMS\Scheduler\Domain\Repository\SchedulerTaskRepository::addExecutionToTask as `array<int, int>`
|
||||
if ($serialized_executions = unserialize($row['serialized_executions'], ['allowed_classes' => false])) {
|
||||
foreach ($serialized_executions as $task) {
|
||||
if ($tstamp - $task < $maxDuration) {
|
||||
$executions[] = $task;
|
||||
} else {
|
||||
try {
|
||||
$schedulerTask = $this->taskSerializer->deserialize($row);
|
||||
$taskType = $schedulerTask->getTaskType();
|
||||
$executionTime = date('Y-m-d H:i:s', $schedulerTask->getExecutionTime());
|
||||
} catch (InvalidTaskException $e) {
|
||||
$taskType = 'unknown type';
|
||||
$executionTime = 'unknown time';
|
||||
}
|
||||
$this->logger->info(
|
||||
'Removing logged execution, assuming that the process is dead. Execution of \'{taskType} \' (UID: {taskId}) was started at {executionTime}',
|
||||
[
|
||||
'taskType' => $taskType,
|
||||
'taskId' => $row['uid'],
|
||||
'executionTime' => $executionTime,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
$executionCount = count($executions);
|
||||
if (!is_array($serialized_executions) || count($serialized_executions) !== $executionCount) {
|
||||
if ($executionCount === 0) {
|
||||
$value = '';
|
||||
} else {
|
||||
$value = serialize($executions);
|
||||
}
|
||||
$this->connectionPool->getConnectionForTable('tx_scheduler_task')->update(
|
||||
'tx_scheduler_task',
|
||||
['serialized_executions' => $value],
|
||||
['uid' => (int)$row['uid']],
|
||||
['serialized_executions' => Connection::PARAM_LOB]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method executes the given task and properly marks and records that execution
|
||||
* It is expected to return FALSE if the task was barred from running or if it was not saved properly
|
||||
*
|
||||
* @param Task\AbstractTask $task The task to execute
|
||||
* @return bool Whether the task was saved successfully to the database or not
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function executeTask(AbstractTask $task): bool
|
||||
{
|
||||
$task->setRunOnNextCronJob(false);
|
||||
// Trigger the saving of the task, as this will calculate its next execution time
|
||||
// This should be calculated all the time, even if the execution is skipped
|
||||
// (in case it is skipped, this pushes back execution to the next possible date)
|
||||
$this->schedulerTaskRepository->updateExecution($task, $task->getExecution()->isSingleRun());
|
||||
|
||||
// Reserve an id for the upcoming execution
|
||||
$executionID = $this->schedulerTaskRepository->addExecutionToTask($task);
|
||||
// Make sure we're the only one executing a single-execution-only task
|
||||
if (!$task->getExecution()?->isParallelExecutionAllowed() && $executionID > 0) {
|
||||
$this->schedulerTaskRepository->removeExecutionOfTask($task, $executionID);
|
||||
$this->logger->info('Task is already running and multiple executions are not allowed, skipping! Task Type: {taskType}, UID: {uid}', [
|
||||
'taskType' => $task->getTaskType(),
|
||||
'uid' => $task->getTaskUid(),
|
||||
]);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Log scheduler invocation
|
||||
$this->logger->info('Start execution. Task Type: {taskType}, UID: {uid}', [
|
||||
'taskType' => $task->getTaskType(),
|
||||
'uid' => $task->getTaskUid(),
|
||||
]);
|
||||
|
||||
$failureString = '';
|
||||
$success = false;
|
||||
$e = null;
|
||||
try {
|
||||
// Execute task
|
||||
$successfullyExecuted = $task->execute();
|
||||
if (!$successfullyExecuted) {
|
||||
throw new FailedExecutionException('Task failed to execute successfully. Task Type: ' . $task->getTaskType() . ', UID: ' . $task->getTaskUid(), 1250596541);
|
||||
}
|
||||
$success = true;
|
||||
return true;
|
||||
} catch (\Throwable $e) {
|
||||
// Log failed execution
|
||||
$this->logger->error('Task failed to execute successfully. Task Type: {taskType}, UID: {taskId}, Code: {code}, "{message}" in {exceptionFile} at line {exceptionLine}', [
|
||||
'taskType' => $task->getTaskType(),
|
||||
'taskId' => $task->getTaskUid(),
|
||||
'exception' => $e,
|
||||
'exceptionFile' => $e->getFile(),
|
||||
'exceptionLine' => $e->getLine(),
|
||||
'code' => $e->getCode(),
|
||||
'message' => $e->getMessage(),
|
||||
]);
|
||||
// Store exception, so that it can be saved to database
|
||||
// Do not serialize the complete exception or the trace, this can lead to huge strings > 50MB
|
||||
$failureString = serialize([
|
||||
'code' => $e->getCode(),
|
||||
'message' => $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'traceString' => $e->getTraceAsString(),
|
||||
]);
|
||||
// Now that the result of the task execution has been handled,
|
||||
// throw the exception again, if any
|
||||
throw $e;
|
||||
} finally {
|
||||
// Un-register execution
|
||||
$this->schedulerTaskRepository->removeExecutionOfTask($task, $executionID, $failureString);
|
||||
// Log completion of execution
|
||||
$this->logger->info('Task executed. Task Type: {taskType}, UID: {uid}', [
|
||||
'taskType' => $task->getTaskType(),
|
||||
'uid' => $task->getTaskUid(),
|
||||
]);
|
||||
$this->eventDispatcher->dispatch(
|
||||
new AfterTaskExecutionEvent($task, $success, $e)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method stores information about the last run of the Scheduler into the system registry
|
||||
*
|
||||
* @param string $type Type of run (manual or command-line (assumed to be cron))
|
||||
*/
|
||||
public function recordLastRun($type = 'cron')
|
||||
{
|
||||
// Validate input value
|
||||
if ($type !== 'manual' && $type !== 'cli-by-id') {
|
||||
$type = 'cron';
|
||||
}
|
||||
$runInformation = ['start' => $GLOBALS['EXEC_TIME'], 'end' => time(), 'type' => $type];
|
||||
$this->registry->set('tx_scheduler', 'lastRun', $runInformation);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
<?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\Service;
|
||||
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
|
||||
use TYPO3\CMS\Core\Console\CommandRegistry;
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
use TYPO3\CMS\Core\Schema\Field\NoneFieldType;
|
||||
use TYPO3\CMS\Core\Schema\Struct\SelectItem;
|
||||
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Scheduler\Exception\InvalidTaskException;
|
||||
use TYPO3\CMS\Scheduler\Task\AbstractTask;
|
||||
use TYPO3\CMS\Scheduler\Task\ExecuteSchedulableCommandTask;
|
||||
|
||||
/**
|
||||
* Service class helping to retrieve data for EXT:scheduler
|
||||
* @internal This is not a public API method, do not use in own extensions
|
||||
*/
|
||||
#[Autoconfigure(public: true)]
|
||||
readonly class TaskService
|
||||
{
|
||||
public function __construct(
|
||||
protected CommandRegistry $commandRegistry,
|
||||
protected TcaSchemaFactory $tcaSchemaFactory,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* This method fetches a list of all classes that have been registered with the Scheduler
|
||||
* For each item the following information is provided, as an associative array:
|
||||
*
|
||||
* ['className'] => Name of the task PHP class
|
||||
* ['extension'] => Key of the extension which provides the class
|
||||
* ['filename'] => Path to the file containing the class
|
||||
* ['title'] => String (possibly localized) containing a human-readable name for the class
|
||||
*
|
||||
* The name of the class itself is used as the key of the list array
|
||||
*/
|
||||
protected function getAvailableTaskTypes(bool $includeNativeTypes = true): array
|
||||
{
|
||||
$languageService = $this->getLanguageService();
|
||||
$list = [];
|
||||
// @deprecated will be removed in v16: SC_OPTIONS-based scheduler task registration
|
||||
// is intentionally still read here in v15 to keep legacy (non-native)
|
||||
// tasks listed and migratable. The per-task ['options']['tables'] fallbacks
|
||||
// in IpAnonymizationTask and TableGarbageCollectionTask are kept for the
|
||||
// same reason. Remove all of this together.
|
||||
foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'] ?? [] as $className => $registrationInformation) {
|
||||
$title = isset($registrationInformation['title']) ? ($languageService?->sL($registrationInformation['title']) ?? $registrationInformation['title']) : '';
|
||||
$description = isset($registrationInformation['description']) ? ($languageService?->sL($registrationInformation['description']) ?? $registrationInformation['description']) : '';
|
||||
$list[$className] = [
|
||||
'className' => $className,
|
||||
'extension' => $registrationInformation['extension'] ?? '',
|
||||
'icon' => $registrationInformation['icon'] ?? '',
|
||||
'title' => $title,
|
||||
'description' => $description,
|
||||
'isNativeTask' => false,
|
||||
'additionalFields' => [],
|
||||
];
|
||||
}
|
||||
if ($includeNativeTypes) {
|
||||
$schema = $this->tcaSchemaFactory->get('tx_scheduler_task');
|
||||
$defaultFields = ['tasktype', 'task_group', 'description', 'parameters', 'execution_details', 'nextexecution', 'lastexecution_context', 'lastexecution_time', 'lastexecution_failure', 'disable', 'priority'];
|
||||
// Loop over TCA items, and check if the task type is registered via TCA
|
||||
foreach ($schema->getField('tasktype')->getConfiguration()['items'] ?? [] as $item) {
|
||||
if (is_array($item) && $item['value'] !== 'div') {
|
||||
$taskType = $className = $item['value'];
|
||||
|
||||
$additionalFields = [];
|
||||
if ($schema->hasSubSchema($taskType)) {
|
||||
$subSchema = $schema->getSubSchema($taskType);
|
||||
if ($subSchema->getRawConfiguration()['taskOptions']['className'] ?? false) {
|
||||
$className = $subSchema->getRawConfiguration()['taskOptions']['className'];
|
||||
}
|
||||
$additionalFields = $subSchema->getFields(fn($field) => !in_array($field->getName(), $defaultFields, true) && $field instanceof NoneFieldType === false);
|
||||
$additionalFields = $additionalFields->getNames();
|
||||
}
|
||||
|
||||
$list[$taskType] = [
|
||||
'taskType' => $taskType,
|
||||
'className' => $className,
|
||||
'extension' => $item['group'] ?? '',
|
||||
'icon' => $item['icon'] ?? '',
|
||||
'iconOverlay' => $item['iconOverlay'] ?? '',
|
||||
'title' => $languageService?->sL($item['label'] ?? '') ?? $item['label'] ?? '',
|
||||
'description' => $languageService?->sL($item['description'] ?? '') ?? $item['description'] ?? '',
|
||||
'isNativeTask' => true,
|
||||
'additionalFields' => $additionalFields,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
return $list;
|
||||
}
|
||||
|
||||
public function getAllTaskTypes(bool $includeNativeTypes = true): array
|
||||
{
|
||||
$taskTypes = [];
|
||||
foreach ($this->getAvailableTaskTypes($includeNativeTypes) as $taskType => $registrationInformation) {
|
||||
$taskTypes[$taskType] = [
|
||||
'className' => $registrationInformation['className'],
|
||||
'taskType' => $taskType,
|
||||
'category' => $registrationInformation['extension'],
|
||||
'icon' => $registrationInformation['icon'],
|
||||
// @todo Remove null coalescing once definition via $GLOBALS['TYPO3_CONF_VARS'] is removed
|
||||
'iconOverlay' => $registrationInformation['iconOverlay'] ?? '',
|
||||
'title' => $registrationInformation['title'],
|
||||
'fullTitle' => $registrationInformation['title'] . ' [' . $registrationInformation['extension'] . ']',
|
||||
'description' => $registrationInformation['description'],
|
||||
'isNativeTask' => $registrationInformation['isNativeTask'],
|
||||
'additionalFields' => $registrationInformation['additionalFields'],
|
||||
];
|
||||
}
|
||||
ksort($taskTypes);
|
||||
return $taskTypes;
|
||||
}
|
||||
|
||||
public function getCategorizedTaskTypes(): array
|
||||
{
|
||||
$categorizedTaskTypes = [];
|
||||
foreach ($this->getAllTaskTypes() as $taskType => $taskInformation) {
|
||||
$categorizedTaskTypes[$taskInformation['category']][$taskType] = $taskInformation;
|
||||
}
|
||||
ksort($categorizedTaskTypes);
|
||||
return $categorizedTaskTypes;
|
||||
}
|
||||
|
||||
public function getTaskDetailsFromTask(AbstractTask $taskObject): ?array
|
||||
{
|
||||
$allTaskTypes = $this->getAllTaskTypes();
|
||||
if (isset($allTaskTypes[$taskObject->getTaskType()])) {
|
||||
return $allTaskTypes[$taskObject->getTaskType()];
|
||||
}
|
||||
if (isset($allTaskTypes[get_class($taskObject)])) {
|
||||
return $allTaskTypes[get_class($taskObject)];
|
||||
}
|
||||
foreach ($allTaskTypes as $taskInformation) {
|
||||
if ($taskInformation['className'] === get_class($taskObject)) {
|
||||
return $taskInformation;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getTaskDetailsFromTaskType(string $taskType): ?array
|
||||
{
|
||||
$allTaskTypes = $this->getAllTaskTypes();
|
||||
if (isset($allTaskTypes[$taskType])) {
|
||||
return $allTaskTypes[$taskType];
|
||||
}
|
||||
foreach ($allTaskTypes as $taskInformation) {
|
||||
if ($taskInformation['className'] === $taskType) {
|
||||
return $taskInformation;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public function isTaskTypeRegistered(string $taskType): bool
|
||||
{
|
||||
$allTaskTypes = $this->getAllTaskTypes();
|
||||
if (isset($allTaskTypes[$taskType])) {
|
||||
return true;
|
||||
}
|
||||
foreach ($allTaskTypes as $taskInformation) {
|
||||
if ($taskInformation['className'] === $taskType) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Native fields are added / managed via FormEngine + dataHandler,
|
||||
* so this only returns additional fields from the task object that are needed.
|
||||
*/
|
||||
public function getFieldsForRecord(AbstractTask $task): array
|
||||
{
|
||||
try {
|
||||
if ($task->getRunOnNextCronJob()) {
|
||||
$executionTime = time();
|
||||
} else {
|
||||
$executionTime = $task->getExecution()->getNextExecution();
|
||||
}
|
||||
$task->setExecutionTime($executionTime);
|
||||
} catch (\Exception) {
|
||||
$task->setDisabled(true);
|
||||
$executionTime = 0;
|
||||
}
|
||||
$fields = [
|
||||
'nextexecution' => $executionTime,
|
||||
'disable' => (int)$task->isDisabled(),
|
||||
'description' => $task->getDescription(),
|
||||
'task_group' => $task->getTaskGroup(),
|
||||
'tasktype' => $task->getTaskType(),
|
||||
'execution_details' => $task->getExecution()->toArray(),
|
||||
];
|
||||
$taskDetails = $this->getTaskDetailsFromTask($task);
|
||||
// Put the parameters in a separate field
|
||||
if (!($taskDetails['isNativeTask'] ?? false)) {
|
||||
$fields['parameters'] = $task->getTaskParameters();
|
||||
}
|
||||
return $fields;
|
||||
}
|
||||
|
||||
public function createNewTask(string $taskType): AbstractTask
|
||||
{
|
||||
if (!$this->isTaskTypeRegistered($taskType)) {
|
||||
throw new InvalidTaskException('Can not create task for unknown type ' . $taskType . '.', 1758885935);
|
||||
}
|
||||
/** @var AbstractTask $task */
|
||||
$task = GeneralUtility::makeInstance($this->getTaskDetailsFromTaskType($taskType)['className']);
|
||||
if ($task instanceof ExecuteSchedulableCommandTask) {
|
||||
$task->setTaskType($taskType);
|
||||
}
|
||||
return $task;
|
||||
}
|
||||
|
||||
public function getHumanReadableTaskName(AbstractTask $task): string
|
||||
{
|
||||
if (!$this->isTaskTypeRegistered($task->getTaskType())) {
|
||||
throw new \RuntimeException('Task Type ' . $task->getTaskType() . ' not found in list of registered tasks', 1641658569);
|
||||
}
|
||||
return $this->getAllTaskTypes()[$task->getTaskType()]['fullTitle'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Used in FormEngine. Actually, this is only needed to task types can be "validated" by form data providers.
|
||||
* There is no possibility to "select" a task type in FormEngine. The field is a readonly information.
|
||||
*/
|
||||
public function getTaskTypesForTcaItems(array &$config, mixed $_ = null, bool $includeNativeItems = false): array
|
||||
{
|
||||
$taskTypes = $this->getAllTaskTypes($includeNativeItems);
|
||||
foreach ($taskTypes as $taskType => $taskInformation) {
|
||||
$config['items'][] = new SelectItem(
|
||||
type: 'select',
|
||||
label: $taskInformation['fullTitle'],
|
||||
value: $taskType,
|
||||
group: $taskInformation['category'],
|
||||
description: $taskInformation['description'],
|
||||
);
|
||||
}
|
||||
// Sort all items by group, and groups as well
|
||||
usort($config['items'], static function (SelectItem $a, SelectItem $b): int {
|
||||
$groupComparison = strnatcasecmp($a->getGroup(), $b->getGroup());
|
||||
if ($groupComparison !== 0) {
|
||||
return $groupComparison;
|
||||
}
|
||||
return strnatcasecmp($a->getLabel(), $b->getLabel());
|
||||
});
|
||||
return $config;
|
||||
}
|
||||
|
||||
private function getLanguageService(): ?LanguageService
|
||||
{
|
||||
return $GLOBALS['LANG'] ?? null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
<?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\SystemInformation;
|
||||
|
||||
use TYPO3\CMS\Backend\Backend\Event\SystemInformationToolbarCollectorEvent;
|
||||
use TYPO3\CMS\Backend\Routing\UriBuilder;
|
||||
use TYPO3\CMS\Backend\Toolbar\InformationStatus;
|
||||
use TYPO3\CMS\Core\Attribute\AsEventListener;
|
||||
use TYPO3\CMS\Core\Domain\DateTimeFactory;
|
||||
use TYPO3\CMS\Core\Localization\DateFormatter;
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
use TYPO3\CMS\Core\Registry;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Scheduler\Domain\Repository\SchedulerTaskRepository;
|
||||
|
||||
/**
|
||||
* Event listener to display information about last automated run, as stored in the system registry.
|
||||
*/
|
||||
final class ToolbarItemProvider
|
||||
{
|
||||
/**
|
||||
* Scheduler last run registry information
|
||||
*/
|
||||
private array $lastRunInformation = [];
|
||||
|
||||
/**
|
||||
* Gather initial information
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly UriBuilder $uriBuilder,
|
||||
Registry $registry,
|
||||
) {
|
||||
$this->lastRunInformation = $registry->get('tx_scheduler', 'lastRun', []);
|
||||
}
|
||||
|
||||
#[AsEventListener('scheduler/show-latest-errors')]
|
||||
public function getItem(SystemInformationToolbarCollectorEvent $event): void
|
||||
{
|
||||
$systemInformationToolbarItem = $event->getToolbarItem();
|
||||
// No tasks configured, so nothing is shown at all
|
||||
if (!$this->hasConfiguredTasks()) {
|
||||
return;
|
||||
}
|
||||
$languageService = $this->getLanguageService();
|
||||
|
||||
if (!$this->schedulerWasExecuted()) {
|
||||
// Display system message if the Scheduler has never yet run
|
||||
$moduleIdentifier = 'scheduler';
|
||||
$systemInformationToolbarItem->addSystemMessage(
|
||||
sprintf(
|
||||
$languageService->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:systemmessage.noLastRun'),
|
||||
(string)$this->uriBuilder->buildUriFromRoute($moduleIdentifier)
|
||||
),
|
||||
InformationStatus::WARNING,
|
||||
1,
|
||||
$moduleIdentifier,
|
||||
);
|
||||
} else {
|
||||
// Display information about the last Scheduler execution
|
||||
if (!$this->lastRunInfoExists()) {
|
||||
// Show warning if the information of the last run is incomplete
|
||||
$message = $languageService->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:msg.incompleteLastRun');
|
||||
$severity = InformationStatus::WARNING;
|
||||
} else {
|
||||
$start = DateTimeFactory::createFromTimestamp($this->lastRunInformation['start']);
|
||||
$end = DateTimeFactory::createFromTimestamp($this->lastRunInformation['end']);
|
||||
$startDate = $start->format($GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy']);
|
||||
$startTime = $start->format($GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm']);
|
||||
$duration = (new DateFormatter())->formatDateInterval(
|
||||
$end->diff($start, true),
|
||||
$languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.minutesHoursDaysYears')
|
||||
);
|
||||
$severity = InformationStatus::NOTICE;
|
||||
$label = 'automatically';
|
||||
if ($this->lastRunInformation['type'] === 'manual') {
|
||||
$label = 'manually';
|
||||
}
|
||||
$type = $languageService->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:label.' . $label);
|
||||
$message = sprintf($languageService->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:systeminformation.lastRunValue'), $startDate, $startTime, $duration, $type);
|
||||
}
|
||||
$systemInformationToolbarItem->addSystemInformation(
|
||||
'LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:systeminformation.lastRunLabel',
|
||||
$message,
|
||||
'actions-play',
|
||||
$severity
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the scheduler was already executed
|
||||
*/
|
||||
private function schedulerWasExecuted(): bool
|
||||
{
|
||||
return !empty($this->lastRunInformation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the last scheduler run array contains all information
|
||||
*/
|
||||
private function lastRunInfoExists(): bool
|
||||
{
|
||||
return !empty($this->lastRunInformation['end'])
|
||||
|| !empty($this->lastRunInformation['start'])
|
||||
|| !empty($this->lastRunInformation['type']);
|
||||
}
|
||||
|
||||
/**
|
||||
* See if there are any tasks configured at all.
|
||||
*/
|
||||
private function hasConfiguredTasks(): bool
|
||||
{
|
||||
return GeneralUtility::makeInstance(SchedulerTaskRepository::class)->hasTasks();
|
||||
}
|
||||
|
||||
private function getLanguageService(): LanguageService
|
||||
{
|
||||
return $GLOBALS['LANG'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,326 @@
|
||||
<?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\Scheduler\Task;
|
||||
|
||||
use Psr\Log\LoggerAwareInterface;
|
||||
use Psr\Log\LoggerAwareTrait;
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
use TYPO3\CMS\Scheduler\Execution;
|
||||
|
||||
/**
|
||||
* This is the base class for all Scheduler tasks
|
||||
* It's an abstract class, not designed to be instantiated directly
|
||||
* All Scheduler tasks should inherit from this class
|
||||
*/
|
||||
abstract class AbstractTask implements LoggerAwareInterface
|
||||
{
|
||||
use LoggerAwareTrait;
|
||||
|
||||
public const TYPE_SINGLE = 1;
|
||||
public const TYPE_RECURRING = 2;
|
||||
|
||||
/**
|
||||
* The unique id of the task used to identify it in the database.
|
||||
*/
|
||||
protected int $taskUid = 0;
|
||||
|
||||
/**
|
||||
* Disable flag, TRUE if task is disabled, FALSE otherwise
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $disabled = false;
|
||||
|
||||
/**
|
||||
* Run on next cron job flag, TRUE if task should run on next cronjob, FALSE otherwise
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $runOnNextCronJob = false;
|
||||
|
||||
/**
|
||||
* The execution object related to the task
|
||||
*
|
||||
* @var Execution
|
||||
*/
|
||||
protected $execution;
|
||||
|
||||
/**
|
||||
* This variable contains the time of next execution of the task
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $executionTime = 0;
|
||||
|
||||
/**
|
||||
* Description for the task
|
||||
*/
|
||||
protected string $description = '';
|
||||
|
||||
/**
|
||||
* Task group for this task
|
||||
*
|
||||
* @var int|null
|
||||
*/
|
||||
protected $taskGroup = 0;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->execution = new Execution();
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the main method that is called when a task is executed
|
||||
* It MUST be implemented by all classes inheriting from this one
|
||||
* Note that there is no error handling, errors and failures are expected
|
||||
* to be handled and logged by the client implementations.
|
||||
* Should return TRUE on successful execution, FALSE on error.
|
||||
*
|
||||
* @return bool Returns TRUE on successful execution, FALSE on error
|
||||
*/
|
||||
abstract public function execute();
|
||||
|
||||
/**
|
||||
* This method is designed to return some additional information about the task,
|
||||
* that may help to set it apart from other tasks from the same class
|
||||
* This additional information is used - for example - in the Scheduler's BE module
|
||||
* This method should be implemented in most task classes
|
||||
*
|
||||
* @return string Information to display
|
||||
*/
|
||||
public function getAdditionalInformation()
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is used to set the unique id of the task
|
||||
*
|
||||
* @param int $id Primary key (from the database record) of the scheduled task
|
||||
*/
|
||||
public function setTaskUid($id): void
|
||||
{
|
||||
$this->taskUid = (int)$id;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method returns the unique id of the task
|
||||
*
|
||||
* @return int The id of the task
|
||||
*/
|
||||
public function getTaskUid(): int
|
||||
{
|
||||
return $this->taskUid;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method returns the disabled status of the task
|
||||
*
|
||||
* @return bool TRUE if task is disabled, FALSE otherwise
|
||||
*/
|
||||
public function isDisabled()
|
||||
{
|
||||
return $this->disabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is used to set the disabled status of the task
|
||||
*
|
||||
* @param bool $flag TRUE if task should be disabled, FALSE otherwise
|
||||
*/
|
||||
public function setDisabled($flag)
|
||||
{
|
||||
if ($flag) {
|
||||
$this->disabled = true;
|
||||
} else {
|
||||
$this->disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method set the flag for next cron job execution
|
||||
*
|
||||
* @param bool $flag TRUE if task should run with the next cron job, FALSE otherwise
|
||||
*/
|
||||
public function setRunOnNextCronJob($flag)
|
||||
{
|
||||
$this->runOnNextCronJob = $flag;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method returns the run on next cron job status of the task
|
||||
*
|
||||
* @return bool TRUE if task should run on next cron job, FALSE otherwise
|
||||
*/
|
||||
public function getRunOnNextCronJob()
|
||||
{
|
||||
return $this->runOnNextCronJob;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is used to set the timestamp corresponding to the next execution time of the task
|
||||
*
|
||||
* @param int $timestamp Timestamp of next execution
|
||||
*/
|
||||
public function setExecutionTime($timestamp)
|
||||
{
|
||||
$this->executionTime = (int)$timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method returns the task group (uid) of the task
|
||||
*
|
||||
* @return int|null Uid of task group or null if it came back from the DB without the task group set.
|
||||
*/
|
||||
public function getTaskGroup()
|
||||
{
|
||||
return $this->taskGroup;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is used to set the task group (uid) of the task
|
||||
*
|
||||
* @param int $taskGroup Uid of task group
|
||||
*/
|
||||
public function setTaskGroup($taskGroup)
|
||||
{
|
||||
$this->taskGroup = (int)$taskGroup;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method returns the timestamp corresponding to the next execution time of the task
|
||||
*
|
||||
* @return int Timestamp of next execution
|
||||
*/
|
||||
public function getExecutionTime()
|
||||
{
|
||||
return $this->executionTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is used to set the description of the task
|
||||
*
|
||||
* @param string $description Description
|
||||
*/
|
||||
public function setDescription($description): void
|
||||
{
|
||||
$this->description = (string)$description;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method returns the description of the task
|
||||
*
|
||||
* @return string Description
|
||||
*/
|
||||
public function getDescription()
|
||||
{
|
||||
return $this->description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the internal execution object
|
||||
*
|
||||
* @param Execution $execution The execution to add
|
||||
* @internal since TYPO3 v12.3, not part of TYPO3 Public API anymore.
|
||||
*/
|
||||
public function setExecution(Execution $execution): void
|
||||
{
|
||||
$this->execution = $execution;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the execution object
|
||||
*
|
||||
* @return Execution|object|null The internal execution object - when an invalid task is being unserialized, the Execution object might not be available
|
||||
* @internal since TYPO3 v12.3, not part of TYPO3 Public API anymore.
|
||||
*/
|
||||
public function getExecution()
|
||||
{
|
||||
return $this->execution;
|
||||
}
|
||||
|
||||
/**
|
||||
* Guess recurring type from the existing information
|
||||
* If an interval or a cron command is defined, it's a recurring task
|
||||
*/
|
||||
public function getType(): int
|
||||
{
|
||||
if ($this->execution->isRecurring()) {
|
||||
return self::TYPE_RECURRING;
|
||||
}
|
||||
return self::TYPE_SINGLE;
|
||||
}
|
||||
|
||||
protected function logException(\Exception $e)
|
||||
{
|
||||
$this->logger?->error('A Task Exception was captured.', ['exception' => $e]);
|
||||
}
|
||||
|
||||
protected function getLanguageService(): ?LanguageService
|
||||
{
|
||||
return $GLOBALS['LANG'] ?? null;
|
||||
}
|
||||
|
||||
public function getTaskType(): string
|
||||
{
|
||||
return static::class;
|
||||
}
|
||||
|
||||
/**
|
||||
* It is recommended to implement this method in the respective task class.
|
||||
*/
|
||||
public function getTaskParameters(): array
|
||||
{
|
||||
$vars = get_object_vars($this);
|
||||
$parameters = [];
|
||||
foreach ($vars as $key => $value) {
|
||||
$key = trim($key);
|
||||
$key = trim($key, "*\0");
|
||||
$key = trim($key);
|
||||
$parameters[$key] = $value;
|
||||
}
|
||||
unset(
|
||||
// Needs to be kept until TYPO3 v16.0 until the upgrade wizard was run through
|
||||
$parameters['scheduler'],
|
||||
$parameters['logger'],
|
||||
$parameters['taskUid'],
|
||||
$parameters['disabled'],
|
||||
$parameters['runOnNextCronJob'],
|
||||
$parameters['execution'],
|
||||
$parameters['executionTime'],
|
||||
$parameters['description'],
|
||||
$parameters['taskGroup'],
|
||||
);
|
||||
return $parameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to fill fields of this class, e.g. also when instantiating this class when no parameters are
|
||||
* given but native DB fields are coming in.
|
||||
* @param array $parameters
|
||||
*/
|
||||
public function setTaskParameters(array $parameters): void
|
||||
{
|
||||
foreach ($parameters as $key => $value) {
|
||||
// Ensure a member property exists; Task objects might have old configuration data changed with
|
||||
// attributes that were removed meanwhile. This would otherwise trigger a PHP notice like
|
||||
// "PHP Runtime Deprecation Notice: Creation of dynamic property TYPO3\CMS\Linkvalidator\Task\ValidatorTask::$fileConfiguration is deprecated"
|
||||
if (property_exists($this, $key)) {
|
||||
$this->{$key} = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
<?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\Scheduler\Task;
|
||||
|
||||
use TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend;
|
||||
use TYPO3\CMS\Core\Cache\CacheManager;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Garbage collection of caching framework cache backends.
|
||||
*
|
||||
* This task finds all configured caching framework caches and
|
||||
* calls the garbage collection of a cache if the cache backend
|
||||
* is configured to be cleaned.
|
||||
* @internal This class is a specific scheduler task implementation is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
class CachingFrameworkGarbageCollectionTask extends AbstractTask
|
||||
{
|
||||
/**
|
||||
* Backend types that should be cleaned up,
|
||||
* set by additional field provider.
|
||||
*
|
||||
* @var array Selected backends to do garbage collection for
|
||||
*/
|
||||
public $selectedBackends = [];
|
||||
|
||||
/**
|
||||
* Execute garbage collection, called by scheduler.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function execute()
|
||||
{
|
||||
// Global sub-array with all configured caches
|
||||
$cacheConfigurations = $GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations'] ?? null;
|
||||
if (is_array($cacheConfigurations)) {
|
||||
// Iterate through configured caches and call garbage collection if
|
||||
// backend is within selected backends in additional field of task
|
||||
foreach ($cacheConfigurations as $cacheName => $cacheConfiguration) {
|
||||
// The cache backend used for this cache
|
||||
$usedCacheBackend = $cacheConfiguration['backend'] ?? Typo3DatabaseBackend::class;
|
||||
if (in_array($usedCacheBackend, $this->selectedBackends, true)) {
|
||||
GeneralUtility::makeInstance(CacheManager::class)->getCache($cacheName)->collectGarbage();
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getTaskParameters(): array
|
||||
{
|
||||
return [
|
||||
'cache_backends' => implode(',', $this->selectedBackends),
|
||||
];
|
||||
}
|
||||
|
||||
public function setTaskParameters(array $parameters): void
|
||||
{
|
||||
$selectedBackends = $parameters['selectedBackends'] ?? $parameters['cache_backends'] ?? [];
|
||||
if (!is_array($selectedBackends)) {
|
||||
$selectedBackends = GeneralUtility::trimExplode(',', $selectedBackends, true);
|
||||
}
|
||||
$this->selectedBackends = $selectedBackends;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all registered caching framework backends
|
||||
*/
|
||||
public function getRegisteredBackends(array &$config): void
|
||||
{
|
||||
$backends = [];
|
||||
$cacheConfigurations = $GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations'];
|
||||
foreach ($cacheConfigurations ?? [] as $cacheConfiguration) {
|
||||
$backend = (string)($cacheConfiguration['backend'] ?? Typo3DatabaseBackend::class);
|
||||
if (!in_array($backend, $backends, true)) {
|
||||
$backends[] = $backend;
|
||||
}
|
||||
}
|
||||
foreach ($backends as $backend) {
|
||||
$config['items'][] = ['value' => $backend, 'label' => $backend];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,310 @@
|
||||
<?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\Task;
|
||||
|
||||
use Symfony\Component\Console\Exception\CommandNotFoundException;
|
||||
use Symfony\Component\Console\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\Console\Exception\InvalidOptionException;
|
||||
use Symfony\Component\Console\Input\ArrayInput;
|
||||
use Symfony\Component\Console\Output\NullOutput;
|
||||
use TYPO3\CMS\Core\Console\CommandRegistry;
|
||||
use TYPO3\CMS\Core\Messaging\FlashMessage;
|
||||
use TYPO3\CMS\Core\Messaging\FlashMessageService;
|
||||
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* @internal This class is a specific scheduler task implementation is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
class ExecuteSchedulableCommandTask extends AbstractTask
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $commandIdentifier = '';
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $arguments = [];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $options = [];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $optionValues = [];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $defaults = [];
|
||||
|
||||
/**
|
||||
* This is the main method that is called when a task is executed
|
||||
* It MUST be implemented by all classes inheriting from this one
|
||||
* Note that there is no error handling, errors and failures are expected
|
||||
* to be handled and logged by the client implementations.
|
||||
* Should return TRUE on successful execution, FALSE on error.
|
||||
*
|
||||
* @throws \Exception
|
||||
*
|
||||
* @return bool Returns TRUE on successful execution, FALSE on error
|
||||
*/
|
||||
public function execute(): bool
|
||||
{
|
||||
try {
|
||||
$commandRegistry = GeneralUtility::makeInstance(CommandRegistry::class);
|
||||
$schedulableCommand = $commandRegistry->get($this->commandIdentifier);
|
||||
} catch (CommandNotFoundException $e) {
|
||||
throw new \RuntimeException(
|
||||
sprintf(
|
||||
$this->getLanguageService()->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:msg.unregisteredCommand'),
|
||||
$this->commandIdentifier
|
||||
),
|
||||
1505055445,
|
||||
$e
|
||||
);
|
||||
}
|
||||
|
||||
$input = new ArrayInput($this->getParameters(false));
|
||||
$input->setInteractive(false);
|
||||
|
||||
$output = new NullOutput();
|
||||
|
||||
return $schedulableCommand->run($input, $output) === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a text representation of the selected command and arguments
|
||||
*
|
||||
* @return string Information to display
|
||||
*/
|
||||
public function getAdditionalInformation(): string
|
||||
{
|
||||
try {
|
||||
$commandRegistry = GeneralUtility::makeInstance(CommandRegistry::class);
|
||||
$schedulableCommand = $commandRegistry->get($this->commandIdentifier);
|
||||
} catch (CommandNotFoundException $e) {
|
||||
return sprintf(
|
||||
$this->getLanguageService()->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:msg.unregisteredCommand'),
|
||||
$this->commandIdentifier
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
$input = new ArrayInput($this->getParameters(true), $schedulableCommand->getDefinition());
|
||||
$arguments = $input->__toString();
|
||||
} catch (\Symfony\Component\Console\Exception\RuntimeException|InvalidArgumentException $e) {
|
||||
return $this->commandIdentifier . "\n"
|
||||
. sprintf(
|
||||
$this->getLanguageService()->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:msg.errorParsingArguments'),
|
||||
$e->getMessage()
|
||||
);
|
||||
} catch (InvalidOptionException $e) {
|
||||
return $this->commandIdentifier . "\n"
|
||||
. sprintf(
|
||||
$this->getLanguageService()->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:msg.errorParsingOptions'),
|
||||
$e->getMessage()
|
||||
);
|
||||
}
|
||||
if ($arguments !== '') {
|
||||
return $this->commandIdentifier . ' ' . $arguments;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
public function getArguments(): array
|
||||
{
|
||||
return $this->arguments;
|
||||
}
|
||||
|
||||
public function getOptions(): array
|
||||
{
|
||||
return $this->options;
|
||||
}
|
||||
|
||||
public function getOptionValues(): array
|
||||
{
|
||||
return $this->optionValues;
|
||||
}
|
||||
|
||||
public function addDefaultValue(string $argumentName, mixed $argumentValue): void
|
||||
{
|
||||
if (is_bool($argumentValue)) {
|
||||
$argumentValue = (int)$argumentValue;
|
||||
}
|
||||
$this->defaults[$argumentName] = $argumentValue;
|
||||
}
|
||||
|
||||
private function getParameters(bool $forDisplay): array
|
||||
{
|
||||
$options = [];
|
||||
foreach ($this->options as $name => $enabled) {
|
||||
if ($enabled) {
|
||||
$value = $this->optionValues[$name] ?? null;
|
||||
$options['--' . $name] = ($forDisplay && $value === true) ? '' : $value;
|
||||
}
|
||||
}
|
||||
return array_merge($this->arguments, $options);
|
||||
}
|
||||
|
||||
public function getTaskType(): string
|
||||
{
|
||||
return $this->commandIdentifier;
|
||||
}
|
||||
|
||||
public function setTaskType(string $taskType): void
|
||||
{
|
||||
$this->commandIdentifier = $taskType;
|
||||
}
|
||||
|
||||
public function getTaskParameters(): array
|
||||
{
|
||||
return [
|
||||
'commandIdentifier' => $this->commandIdentifier,
|
||||
'arguments' => $this->arguments,
|
||||
'options' => $this->options,
|
||||
'optionValues' => $this->optionValues,
|
||||
];
|
||||
}
|
||||
public function setTaskParameters(array $parameters): void
|
||||
{
|
||||
$this->commandIdentifier = $parameters['commandIdentifier'] ?? $this->commandIdentifier;
|
||||
$this->arguments = $this->processArguments($parameters);
|
||||
$processedOptions = $this->processOptions($parameters);
|
||||
$this->options = $processedOptions['options'] ?? [];
|
||||
$this->optionValues = $processedOptions['optionValues'] ?? [];
|
||||
}
|
||||
|
||||
public function validateTaskParameters(array $parameters): bool
|
||||
{
|
||||
$result = true;
|
||||
$commandRegistry = GeneralUtility::makeInstance(CommandRegistry::class);
|
||||
$flashMessageQueue = GeneralUtility::makeInstance(FlashMessageService::class)->getMessageQueueByIdentifier();
|
||||
if ($commandRegistry->has($this->getTaskType())
|
||||
&& (is_array($parameters['arguments'] ?? false) || is_array($parameters['options'] ?? false))
|
||||
) {
|
||||
// If this is a registered console command, validate given arguments / options
|
||||
$command = $commandRegistry->get($this->getTaskType());
|
||||
foreach ($command->getDefinition()->getArguments() as $argument) {
|
||||
foreach (($parameters['arguments'] ?? []) as $argumentName => $argumentValue) {
|
||||
if ($argument->getName() !== $argumentName) {
|
||||
continue;
|
||||
}
|
||||
if ($argument->isRequired() && trim($argumentValue) === '') {
|
||||
$flashMessageQueue->addMessage(
|
||||
new FlashMessage(sprintf(
|
||||
$this->getLanguageService()?->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:msg.mandatoryArgumentMissing'),
|
||||
$argumentName
|
||||
), '', ContextualFeedbackSeverity::ERROR)
|
||||
);
|
||||
$result = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach ($command->getDefinition()->getOptions() as $optionDefinition) {
|
||||
$optionEnabled = $parameters['options'][$optionDefinition->getName()] ?? false;
|
||||
$optionValue = $parameters['optionValues'][$optionDefinition->getName()] ?? $optionDefinition->getDefault();
|
||||
if ($optionEnabled && $optionDefinition->isValueRequired()) {
|
||||
if ($optionDefinition->isArray()) {
|
||||
$testValues = is_array($optionValue) ? $optionValue : GeneralUtility::trimExplode(',', $optionValue, false);
|
||||
} else {
|
||||
$testValues = [$optionValue];
|
||||
}
|
||||
foreach ($testValues as $testValue) {
|
||||
if ($testValue === null || trim($testValue) === '') {
|
||||
// An option that requires a value is used with an empty value
|
||||
$flashMessageQueue->addMessage(
|
||||
new FlashMessage(sprintf(
|
||||
$this->getLanguageService()?->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:msg.mandatoryArgumentMissing'),
|
||||
$optionDefinition->getName()
|
||||
), '', ContextualFeedbackSeverity::ERROR)
|
||||
);
|
||||
$result = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
protected function processArguments(array $paremeters): array
|
||||
{
|
||||
if (!is_array($paremeters['arguments'] ?? false)) {
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
$commandRegistry = GeneralUtility::makeInstance(CommandRegistry::class);
|
||||
$command = $commandRegistry->get($this->commandIdentifier);
|
||||
} catch (CommandNotFoundException) {
|
||||
return [];
|
||||
}
|
||||
$arguments = [];
|
||||
foreach ($paremeters['arguments'] as $argumentName => $argumentValue) {
|
||||
try {
|
||||
$argumentDefinition = $command->getDefinition()->getArgument($argumentName);
|
||||
} catch (InvalidArgumentException) {
|
||||
continue;
|
||||
}
|
||||
if ($argumentDefinition->isArray() && is_string($argumentValue)) {
|
||||
$argumentValue = GeneralUtility::trimExplode(',', $argumentValue, true);
|
||||
}
|
||||
$arguments[$argumentName] = $argumentValue;
|
||||
}
|
||||
return $arguments;
|
||||
}
|
||||
|
||||
protected function processOptions(array $parameters): array
|
||||
{
|
||||
if (!is_array($parameters['options'] ?? false)) {
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
$commandRegistry = GeneralUtility::makeInstance(CommandRegistry::class);
|
||||
$command = $commandRegistry->get($this->commandIdentifier);
|
||||
} catch (CommandNotFoundException) {
|
||||
return [];
|
||||
}
|
||||
$options = [];
|
||||
$optionValues = [];
|
||||
foreach ($command->getDefinition()->getOptions() as $optionDefinition) {
|
||||
$optionEnabled = $parameters['options'][$optionDefinition->getName()] ?? false;
|
||||
$options[$optionDefinition->getName()] = (bool)$optionEnabled;
|
||||
if ($optionDefinition->isValueRequired() || $optionDefinition->isValueOptional() || $optionDefinition->isArray()) {
|
||||
$optionValue = $parameters['optionValues'][$optionDefinition->getName()] ?? $optionDefinition->getDefault();
|
||||
if ($optionDefinition->isArray() && is_string($optionValue)) {
|
||||
// Do not remove empty array values.
|
||||
// One empty array element indicates the existence of one occurrence of an array option (InputOption::VALUE_IS_ARRAY) without a value.
|
||||
// Empty array elements are also required for command options like "-vvv" (can be entered as ",,").
|
||||
$optionValue = GeneralUtility::trimExplode(',', $optionValue);
|
||||
}
|
||||
} else {
|
||||
// boolean flag: option value must be true if option is added or false otherwise
|
||||
$optionValue = (bool)$optionEnabled;
|
||||
}
|
||||
$optionValues[$optionDefinition->getName()] = $optionValue;
|
||||
}
|
||||
return ['options' => $options, 'optionValues' => $optionValues];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
<?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\Scheduler\Task;
|
||||
|
||||
use TYPO3\CMS\Core\Resource\Index\Indexer;
|
||||
use TYPO3\CMS\Core\Resource\ResourceStorage;
|
||||
use TYPO3\CMS\Core\Resource\StorageRepository;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* This task which indexes files in storage
|
||||
* @internal This class is a specific scheduler task implementation is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
class FileStorageExtractionTask extends AbstractTask
|
||||
{
|
||||
/**
|
||||
* Storage Uid
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $storageUid = -1;
|
||||
|
||||
/**
|
||||
* FileCount
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $maxFileCount = 100;
|
||||
|
||||
/**
|
||||
* Function execute from the Scheduler
|
||||
*
|
||||
* @return bool TRUE on successful execution, FALSE on error
|
||||
*/
|
||||
public function execute()
|
||||
{
|
||||
$success = false;
|
||||
if ((int)$this->storageUid > 0) {
|
||||
$storage = GeneralUtility::makeInstance(StorageRepository::class)->findByUid($this->storageUid);
|
||||
if ($storage === null) {
|
||||
throw new \RuntimeException(self::class . ' misconfiguration: "Storage to index" must be an existing storage.', 1615020909);
|
||||
}
|
||||
$currentEvaluatePermissionsValue = $storage->getEvaluatePermissions();
|
||||
$storage->setEvaluatePermissions(false);
|
||||
$indexer = $this->getIndexer($storage);
|
||||
try {
|
||||
$indexer->runMetaDataExtraction((int)$this->maxFileCount);
|
||||
$success = true;
|
||||
} catch (\Exception $e) {
|
||||
$success = false;
|
||||
$this->logException($e);
|
||||
}
|
||||
$storage->setEvaluatePermissions($currentEvaluatePermissionsValue);
|
||||
}
|
||||
return $success;
|
||||
}
|
||||
|
||||
protected function getIndexer(ResourceStorage $storage): Indexer
|
||||
{
|
||||
return GeneralUtility::makeInstance(Indexer::class, $storage);
|
||||
}
|
||||
|
||||
public function getTaskParameters(): array
|
||||
{
|
||||
return [
|
||||
'file_storage' => $this->storageUid,
|
||||
'max_file_count' => $this->maxFileCount,
|
||||
];
|
||||
}
|
||||
public function setTaskParameters(array $parameters): void
|
||||
{
|
||||
$this->storageUid = (int)($parameters['storageUid'] ?? $parameters['file_storage'] ?? -1);
|
||||
$this->maxFileCount = (int)($parameters['maxFileCount'] ?? $parameters['max_file_count'] ?? 100);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<?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\Scheduler\Task;
|
||||
|
||||
use TYPO3\CMS\Core\Resource\Index\Indexer;
|
||||
use TYPO3\CMS\Core\Resource\ResourceStorage;
|
||||
use TYPO3\CMS\Core\Resource\StorageRepository;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* This task tries to find changes in storage and writes them back to DB
|
||||
* @internal This class is a specific scheduler task implementation is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
class FileStorageIndexingTask extends AbstractTask
|
||||
{
|
||||
/**
|
||||
* Storage Uid
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $storageUid = -1;
|
||||
|
||||
/**
|
||||
* Function execute from the Scheduler
|
||||
*
|
||||
* @return bool TRUE on successful execution, FALSE on error
|
||||
*/
|
||||
public function execute()
|
||||
{
|
||||
if ((int)$this->storageUid > 0) {
|
||||
$storage = GeneralUtility::makeInstance(StorageRepository::class)->findByUid($this->storageUid);
|
||||
if ($storage === null) {
|
||||
throw new \RuntimeException(self::class . ' misconfiguration: "Storage to index" must be an existing storage.', 1615020908);
|
||||
}
|
||||
$currentEvaluatePermissionsValue = $storage->getEvaluatePermissions();
|
||||
$storage->setEvaluatePermissions(false);
|
||||
$indexer = $this->getIndexer($storage);
|
||||
$indexer->processChangesInStorages();
|
||||
$storage->setEvaluatePermissions($currentEvaluatePermissionsValue);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function getIndexer(ResourceStorage $storage): Indexer
|
||||
{
|
||||
return GeneralUtility::makeInstance(Indexer::class, $storage);
|
||||
}
|
||||
|
||||
public function getTaskParameters(): array
|
||||
{
|
||||
return [
|
||||
'file_storage' => $this->storageUid,
|
||||
];
|
||||
}
|
||||
|
||||
public function setTaskParameters(array $parameters): void
|
||||
{
|
||||
$this->storageUid = $parameters['storageUid'] ?? $parameters['file_storage'] ?? 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
<?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\Task;
|
||||
|
||||
use TYPO3\CMS\Core\Database\Connection;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Utility\IpAnonymizationUtility;
|
||||
|
||||
/**
|
||||
* Anonymize IP addresses in records
|
||||
*
|
||||
* This task anonymizes IP addresses in tables older than the given number of days.
|
||||
*
|
||||
* Available tables must be registered in
|
||||
* $GLOBALS['TCA']['tx_scheduler_task']['types'][\TYPO3\CMS\Scheduler\Task\IpAnonymizationTask::class]['taskOptions']['tables']
|
||||
*
|
||||
* See scheduler_ip_anonymization_task.php of scheduler extension for an example.
|
||||
*
|
||||
* @internal This class is a specific scheduler task implementation is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
class IpAnonymizationTask extends AbstractTask
|
||||
{
|
||||
/**
|
||||
* @var int Number of days
|
||||
*/
|
||||
public $numberOfDays = 180;
|
||||
|
||||
/**
|
||||
* @var int mask level see \TYPO3\CMS\Core\Utility\IpAnonymizationUtility::anonymizeIp
|
||||
*/
|
||||
public $mask = 2;
|
||||
|
||||
/**
|
||||
* @var string Table to clean up
|
||||
*/
|
||||
public $table = '';
|
||||
|
||||
/**
|
||||
* Execute garbage collection, called by scheduler.
|
||||
*
|
||||
* @throws \RuntimeException If configured table was not cleaned up
|
||||
* @return bool TRUE if task run was successful
|
||||
*/
|
||||
public function execute()
|
||||
{
|
||||
$configuration = $this->getTableConfiguration()[$this->table] ?? [];
|
||||
if (empty($configuration)) {
|
||||
throw new \RuntimeException(self::class . ' misconfiguration: ' . $this->table . ' does not exist in configuration', 1524502548);
|
||||
}
|
||||
$this->handleTable($this->table, $configuration);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute clean up of a specific table
|
||||
*
|
||||
* @throws \RuntimeException If table configuration is broken
|
||||
* @param string $table The table to handle
|
||||
* @param array $configuration Clean up configuration
|
||||
* @return bool TRUE if cleanup was successful
|
||||
*/
|
||||
protected function handleTable(string $table, array $configuration)
|
||||
{
|
||||
$connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
|
||||
$queryBuilder = $connection->createQueryBuilder();
|
||||
$queryBuilder->getRestrictions()->removeAll();
|
||||
|
||||
if (empty($configuration['dateField'])) {
|
||||
throw new \RuntimeException(self::class . ' misconfiguration: "dateField" must be defined for table ' . $table, 1524502549);
|
||||
}
|
||||
if (empty($configuration['ipField'])) {
|
||||
throw new \RuntimeException(self::class . ' misconfiguration: "ipField" must be defined for table ' . $table, 1524502666);
|
||||
}
|
||||
$deleteTimestamp = strtotime('-' . $this->numberOfDays . 'days');
|
||||
if ($deleteTimestamp === false) {
|
||||
throw new \RuntimeException(self::class . ' misconfiguration: number of days could not be calculated for table ' . $table, 1524526354);
|
||||
}
|
||||
if ($this->mask === 2) {
|
||||
$notLikeMaskPattern = '%.0.0';
|
||||
} else {
|
||||
$notLikeMaskPattern = '%.0';
|
||||
}
|
||||
try {
|
||||
$result = $queryBuilder
|
||||
->select('uid', $configuration['ipField'])
|
||||
->where(
|
||||
$queryBuilder->expr()->lt(
|
||||
$configuration['dateField'],
|
||||
$queryBuilder->createNamedParameter($deleteTimestamp, Connection::PARAM_INT)
|
||||
),
|
||||
$queryBuilder->expr()->neq(
|
||||
$configuration['ipField'],
|
||||
$queryBuilder->createNamedParameter('')
|
||||
),
|
||||
$queryBuilder->expr()->isNotNull($configuration['ipField']),
|
||||
$queryBuilder->expr()->notLike(
|
||||
$configuration['ipField'],
|
||||
$queryBuilder->createNamedParameter($notLikeMaskPattern)
|
||||
),
|
||||
$queryBuilder->expr()->notLike(
|
||||
$configuration['ipField'],
|
||||
$queryBuilder->createNamedParameter('%::')
|
||||
)
|
||||
)
|
||||
->from($table)
|
||||
->executeQuery();
|
||||
|
||||
while ($row = $result->fetchAssociative()) {
|
||||
$ip = (string)$row[$configuration['ipField']];
|
||||
|
||||
$connection->update(
|
||||
$table,
|
||||
[
|
||||
$configuration['ipField'] => IpAnonymizationUtility::anonymizeIp($ip, (int)$this->mask),
|
||||
],
|
||||
[
|
||||
'uid' => $row['uid'],
|
||||
]
|
||||
);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
throw new \RuntimeException(self::class . ' failed for table ' . $this->table . ' with error: ' . $e->getMessage(), 1524502550);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getAdditionalInformation()
|
||||
{
|
||||
return sprintf($this->getLanguageService()->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:label.ipAnonymization.additionalInformationTable'), $this->table, $this->numberOfDays);
|
||||
}
|
||||
|
||||
public function getTaskParameters(): array
|
||||
{
|
||||
return [
|
||||
'number_of_days' => $this->numberOfDays,
|
||||
'ip_mask' => $this->mask,
|
||||
'selected_tables' => $this->table,
|
||||
];
|
||||
}
|
||||
|
||||
public function setTaskParameters(array $parameters): void
|
||||
{
|
||||
$this->table = (string)($parameters['table'] ?? $parameters['selected_tables'] ?? '');
|
||||
$this->numberOfDays = (int)($parameters['numberOfDays'] ?? $parameters['number_of_days'] ?? 180);
|
||||
$this->mask = (int)($parameters['mask'] ?? $parameters['ip_mask'] ?? 2);
|
||||
}
|
||||
|
||||
public function getAnonymizableTables(array &$config): void
|
||||
{
|
||||
foreach ($this->getTableConfiguration() as $tableName => $tableConfiguration) {
|
||||
$config['items'][] = [
|
||||
'label' => $tableName . (($tableConfiguration['ipField'] ?? false) ? ' [ipField: ' . $tableConfiguration['ipField'] . ']' : '') . (($tableConfiguration['dateField'] ?? false) ? ' [dateField: ' . $tableConfiguration['dateField'] . ']' : ''),
|
||||
'value' => $tableName,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
public function getTableConfiguration(): array
|
||||
{
|
||||
$tableConfiguration = GeneralUtility::makeInstance(TcaSchemaFactory::class)->get('tx_scheduler_task.' . self::class)->getRawConfiguration()['taskOptions']['tables'] ?? [];
|
||||
|
||||
$tableConfigurationFromConfVars = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][self::class]['options']['tables'] ?? [];
|
||||
if (!empty($tableConfigurationFromConfVars)) {
|
||||
// @deprecated will be removed in v16: this SC_OPTIONS fallback is intentionally
|
||||
// kept beyond v15 because SC_OPTIONS-based scheduler task registration
|
||||
// is still read in TaskService::getAvailableTaskTypes() to keep legacy
|
||||
// (non-native) tasks migratable. Remove together with that support.
|
||||
trigger_error('Usage of $GLOBALS[\'TYPO3_CONF_VARS\'][\'SC_OPTIONS\'][\'scheduler\'][\'tasks\'][' . self::class . '][\'options\'][\'tables\'] to define table options is deprecated and will stop working in TYPO3 v16. Use $tca[\'tx_scheduler_task\'][\'types\'][' . self::class . '][\'taskOptions\'][\'tables\'] instead.', E_USER_DEPRECATED);
|
||||
if (is_array($tableConfigurationFromConfVars)) {
|
||||
$tableConfiguration = array_replace_recursive($tableConfiguration, $tableConfigurationFromConfVars);
|
||||
}
|
||||
}
|
||||
|
||||
return $tableConfiguration;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
<?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\Scheduler\Task;
|
||||
|
||||
use Doctrine\DBAL\Exception as DBALException;
|
||||
use Doctrine\DBAL\Platforms\MariaDBPlatform as DoctrineMariaDBPlatform;
|
||||
use Doctrine\DBAL\Platforms\MySQLPlatform as DoctrineMySQLPlatform;
|
||||
use TYPO3\CMS\Core\Database\Connection;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Perform OPTIMIZE TABLE SQL statements
|
||||
*
|
||||
* This task reorganizes the physical storage of table data and associated index data,
|
||||
* to reduce storage space and improve I/O efficiency when accessing the table. The
|
||||
* exact changes made to each table depend on the storage engine used by that table.
|
||||
* @internal This class is a specific scheduler task implementation is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
class OptimizeDatabaseTableTask extends AbstractTask
|
||||
{
|
||||
/**
|
||||
* Database tables that should be cleaned up,
|
||||
* set by additional field provider.
|
||||
*
|
||||
* @var array Selected tables to optimize
|
||||
*/
|
||||
public $selectedTables = [];
|
||||
|
||||
/**
|
||||
* Execute table optimization, called by scheduler.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function execute()
|
||||
{
|
||||
$connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
|
||||
foreach ($this->selectedTables as $tableName) {
|
||||
$connection = $connectionPool->getConnectionForTable($tableName);
|
||||
$platform = $connection->getDatabasePlatform();
|
||||
|
||||
if ($platform instanceof DoctrineMariaDBPlatform || $platform instanceof DoctrineMySQLPlatform) {
|
||||
try {
|
||||
// `OPTIMIZE TABLE` returns a result set and must be executed using `executeQuery()`,
|
||||
// otherwise following database queries would fail with a database exception because of a
|
||||
// not-consumed query buffer with `pdo_mysql` driver and the full result set is retrieved
|
||||
// with `fetchAllAssociative()` and discarded as handling is not intended here.
|
||||
$connection->executeQuery('OPTIMIZE TABLE ' . $connection->quoteIdentifier($tableName))->fetchAllAssociative();
|
||||
} catch (DBALException $e) {
|
||||
throw new \RuntimeException(
|
||||
TableGarbageCollectionTask::class . ' failed for: ' . $tableName . ': '
|
||||
. $e->getMessage(),
|
||||
1441390263
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Output the selected tables
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getAdditionalInformation()
|
||||
{
|
||||
return implode(', ', $this->selectedTables);
|
||||
}
|
||||
|
||||
public function getTaskParameters(): array
|
||||
{
|
||||
return [
|
||||
'selected_tables' => implode(',', $this->selectedTables),
|
||||
];
|
||||
}
|
||||
|
||||
public function setTaskParameters(array $parameters): void
|
||||
{
|
||||
$selectedTables = $parameters['selected_tables'] ?? $parameters['tables'] ?? [];
|
||||
if (!is_array($selectedTables)) {
|
||||
$selectedTables = GeneralUtility::trimExplode(',', $selectedTables, true);
|
||||
}
|
||||
$this->selectedTables = $selectedTables;
|
||||
}
|
||||
|
||||
/**
|
||||
* TCA itemsProcFunc
|
||||
* Get all tables that are capable of optimization
|
||||
*/
|
||||
public function getOptimizableTables(array &$config): array
|
||||
{
|
||||
$connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
|
||||
$defaultConnection = $connectionPool->getConnectionByName(ConnectionPool::DEFAULT_CONNECTION_NAME);
|
||||
|
||||
// Retrieve all optimizable tables for the default connection
|
||||
$optimizableTables = $this->getOptimizableTablesForConnection($defaultConnection);
|
||||
|
||||
// Retrieve additional optimizable tables that have been remapped to a different connection
|
||||
$tableMap = $GLOBALS['TYPO3_CONF_VARS']['DB']['TableMapping'] ?? [];
|
||||
if ($tableMap) {
|
||||
// Remove all remapped tables from the list of optimizable tables
|
||||
// These tables will be rechecked and possibly re-added to the list
|
||||
// of optimizable tables. This ensures that no orphaned table from
|
||||
// the default connection gets mistakenly labeled as optimizable.
|
||||
$optimizableTables = array_diff($optimizableTables, array_keys($tableMap));
|
||||
|
||||
// Walk each connection and check all tables that have been
|
||||
// remapped to it for optimization support.
|
||||
$connectionNames = array_keys(array_flip($tableMap));
|
||||
foreach ($connectionNames as $connectionName) {
|
||||
$connection = $connectionPool->getConnectionByName($connectionName);
|
||||
$tablesOnConnection = array_keys(array_filter(
|
||||
$tableMap,
|
||||
static function ($value) use ($connectionName) {
|
||||
return $value === $connectionName;
|
||||
}
|
||||
));
|
||||
$tables = $this->getOptimizableTablesForConnection($connection, $tablesOnConnection);
|
||||
$optimizableTables = array_merge($optimizableTables, $tables);
|
||||
}
|
||||
}
|
||||
|
||||
sort($optimizableTables);
|
||||
foreach ($optimizableTables as $tableName) {
|
||||
$config['items'][] = [
|
||||
'label' => $tableName,
|
||||
'value' => $tableName,
|
||||
];
|
||||
}
|
||||
return $optimizableTables;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve all optimizable tables for a connection, optionally restricted to the subset
|
||||
* of table names in the $tableNames array.
|
||||
*/
|
||||
protected function getOptimizableTablesForConnection(Connection $connection, array $tableNames = []): array
|
||||
{
|
||||
// Return empty list if the database platform is not MySQL/MariaDB
|
||||
$platform = $connection->getDatabasePlatform();
|
||||
if (!($platform instanceof DoctrineMariaDBPlatform || $platform instanceof DoctrineMySQLPlatform)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Retrieve all tables from the MySQL information schema that have an engine type
|
||||
// that supports the OPTIMIZE TABLE command.
|
||||
$queryBuilder = $connection->createQueryBuilder();
|
||||
$queryBuilder->select('TABLE_NAME AS Table', 'ENGINE AS Engine')
|
||||
->from('information_schema.TABLES')
|
||||
->where(
|
||||
$queryBuilder->expr()->eq(
|
||||
'TABLE_TYPE',
|
||||
$queryBuilder->createNamedParameter('BASE TABLE')
|
||||
),
|
||||
$queryBuilder->expr()->in(
|
||||
'ENGINE',
|
||||
$queryBuilder->createNamedParameter(['InnoDB', 'MyISAM', 'ARCHIVE'], Connection::PARAM_STR_ARRAY)
|
||||
),
|
||||
$queryBuilder->expr()->eq(
|
||||
'TABLE_SCHEMA',
|
||||
$queryBuilder->createNamedParameter($connection->getDatabase())
|
||||
)
|
||||
);
|
||||
|
||||
if (!empty($tableNames)) {
|
||||
$queryBuilder->andWhere(
|
||||
$queryBuilder->expr()->in(
|
||||
'TABLE_NAME',
|
||||
$queryBuilder->createNamedParameter($tableNames, Connection::PARAM_STR_ARRAY)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$tables = $queryBuilder->executeQuery()->fetchAllAssociative();
|
||||
|
||||
return array_column($tables, 'Table');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
<?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\Task;
|
||||
|
||||
use TYPO3\CMS\Core\Resource\Folder;
|
||||
use TYPO3\CMS\Core\Resource\StorageRepository;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Recycler folder garbage collection task
|
||||
*
|
||||
* This task finds all "_recycler_" folders below all storages and
|
||||
* deletes all files in them that have not changed for more than
|
||||
* a given number of days.
|
||||
*
|
||||
* Compatible drivers should be implemented correctly for this. The shipped "local driver"
|
||||
* does a "touch()" after the file is moved into the recycler folder.
|
||||
* @internal This class is a specific scheduler task implementation is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
class RecyclerGarbageCollectionTask extends AbstractTask
|
||||
{
|
||||
/**
|
||||
* Elapsed period since last modification before a file will
|
||||
* be deleted in a recycler directory.
|
||||
*
|
||||
* @var int Number of days before cleaning up files
|
||||
*/
|
||||
public $numberOfDays = 0;
|
||||
|
||||
/**
|
||||
* Cleanup recycled files, called by scheduler.
|
||||
*
|
||||
* @return bool TRUE if task run was successful
|
||||
* @throws \BadMethodCallException
|
||||
*/
|
||||
public function execute()
|
||||
{
|
||||
$recyclerFolders = [];
|
||||
$storageRepository = GeneralUtility::makeInstance(StorageRepository::class);
|
||||
// takes only _recycler_ folder on the first level into account
|
||||
foreach ($storageRepository->findAll() as $storage) {
|
||||
$rootLevelFolder = $storage->getRootLevelFolder(false);
|
||||
foreach ($rootLevelFolder->getSubfolders() as $subFolder) {
|
||||
if ($subFolder->getRole() === $subFolder::ROLE_RECYCLER) {
|
||||
$recyclerFolders[] = $subFolder;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Execute cleanup
|
||||
$seconds = 60 * 60 * 24 * (int)$this->numberOfDays;
|
||||
$timestamp = $GLOBALS['EXEC_TIME'] - $seconds;
|
||||
foreach ($recyclerFolders as $recyclerFolder) {
|
||||
$this->cleanupRecycledFiles($recyclerFolder, $timestamp);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list of all files in a directory recursively and removes
|
||||
* old ones.
|
||||
*
|
||||
* @param Folder $folder the folder
|
||||
* @param int $timestamp Timestamp of the last file modification
|
||||
*/
|
||||
protected function cleanupRecycledFiles(Folder $folder, $timestamp)
|
||||
{
|
||||
foreach ($folder->getFiles() as $file) {
|
||||
if ($timestamp > $file->getModificationTime()) {
|
||||
$file->delete();
|
||||
}
|
||||
}
|
||||
foreach ($folder->getSubfolders() as $subFolder) {
|
||||
$this->cleanupRecycledFiles($subFolder, $timestamp);
|
||||
// if no more files and subdirectories are in the folder, remove the folder as well
|
||||
if ($subFolder->getFileCount() === 0 && count($subFolder->getSubfolders()) === 0) {
|
||||
$subFolder->delete(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function getTaskParameters(): array
|
||||
{
|
||||
return [
|
||||
'number_of_days' => $this->numberOfDays,
|
||||
];
|
||||
}
|
||||
|
||||
public function setTaskParameters(array $parameters): void
|
||||
{
|
||||
$this->numberOfDays = (int)($parameters['numberOfDays'] ?? $parameters['number_of_days'] ?? 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
<?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\Task;
|
||||
|
||||
use Doctrine\DBAL\Exception as DBALException;
|
||||
use TYPO3\CMS\Core\Database\Connection;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Remove old entries from tables.
|
||||
*
|
||||
* This task deletes rows from tables older than the given number of days.
|
||||
*
|
||||
* Available tables must be registered in
|
||||
* $GLOBALS['TCA']['tx_scheduler_task']['types'][\TYPO3\CMS\Scheduler\Task\TableGarbageCollectionTask::class]['taskOptions']['tables']
|
||||
*
|
||||
* See scheduler_table_garbage_collection_task.php of scheduler extension for an example.
|
||||
*
|
||||
* @internal This class is a specific scheduler task implementation is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
class TableGarbageCollectionTask extends AbstractTask
|
||||
{
|
||||
/**
|
||||
* @var bool True if all tables should be cleaned up
|
||||
*/
|
||||
public $allTables = false;
|
||||
|
||||
/**
|
||||
* @var int Number of days
|
||||
*/
|
||||
public $numberOfDays = 0;
|
||||
|
||||
/**
|
||||
* @var string Table to clean up
|
||||
*/
|
||||
public $table = '';
|
||||
|
||||
/**
|
||||
* Execute garbage collection, called by scheduler.
|
||||
*
|
||||
* @throws \RuntimeException If configured table was not cleaned up
|
||||
* @return bool TRUE if task run was successful
|
||||
*/
|
||||
public function execute()
|
||||
{
|
||||
$tableConfigurations = $this->getTableConfiguration();
|
||||
$tableHandled = false;
|
||||
foreach ($tableConfigurations as $tableName => $configuration) {
|
||||
if ($this->allTables || $tableName === $this->table) {
|
||||
$this->handleTable($tableName, $configuration);
|
||||
$tableHandled = true;
|
||||
}
|
||||
}
|
||||
if (!$tableHandled) {
|
||||
throw new \RuntimeException(self::class . ' misconfiguration: ' . $this->table . ' does not exist in configuration', 1308354399);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute clean up of a specific table
|
||||
*
|
||||
* @throws \RuntimeException If table configuration is broken
|
||||
* @param string $table The table to handle
|
||||
* @param array $configuration Clean up configuration
|
||||
* @return bool TRUE if cleanup was successful
|
||||
*/
|
||||
protected function handleTable(string $table, array $configuration): bool
|
||||
{
|
||||
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
|
||||
$queryBuilder->delete($table);
|
||||
if (!empty($configuration['expireField'])) {
|
||||
$field = $configuration['expireField'];
|
||||
$dateLimit = $GLOBALS['EXEC_TIME'];
|
||||
// If expire field value is 0, do not delete
|
||||
// Expire field = 0 means no expiration
|
||||
$queryBuilder->where(
|
||||
$queryBuilder->expr()->lte($field, $queryBuilder->createNamedParameter($dateLimit, Connection::PARAM_INT)),
|
||||
$queryBuilder->expr()->gt($field, $queryBuilder->createNamedParameter(0, Connection::PARAM_INT))
|
||||
);
|
||||
} elseif (!empty($configuration['dateField'])) {
|
||||
if (!$this->allTables) {
|
||||
$numberOfDays = $this->numberOfDays;
|
||||
if (isset($configuration['expirePeriod']) && $numberOfDays <= 0) {
|
||||
$numberOfDays = (int)$configuration['expirePeriod'];
|
||||
}
|
||||
$deleteTimestamp = strtotime('-' . $numberOfDays . 'days');
|
||||
} else {
|
||||
if (!isset($configuration['expirePeriod'])) {
|
||||
throw new \RuntimeException(self::class . ' misconfiguration: No expirePeriod defined for table ' . $table, 1308355095);
|
||||
}
|
||||
$deleteTimestamp = strtotime('-' . $configuration['expirePeriod'] . 'days');
|
||||
}
|
||||
$queryBuilder->where(
|
||||
$queryBuilder->expr()->lt(
|
||||
$configuration['dateField'],
|
||||
$queryBuilder->createNamedParameter($deleteTimestamp, Connection::PARAM_INT)
|
||||
)
|
||||
);
|
||||
} else {
|
||||
throw new \RuntimeException(self::class . ' misconfiguration: Either expireField or dateField must be defined for table ' . $table, 1308355268);
|
||||
}
|
||||
|
||||
try {
|
||||
$queryBuilder->executeStatement();
|
||||
} catch (DBALException $e) {
|
||||
throw new \RuntimeException(self::class . ' failed for table ' . $this->table . ' with error: ' . $e->getMessage(), 1308255491);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method returns the selected table as additional information
|
||||
*
|
||||
* @return string Information to display
|
||||
*/
|
||||
public function getAdditionalInformation()
|
||||
{
|
||||
if ($this->allTables) {
|
||||
$message = $this->getLanguageService()?->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:label.tableGarbageCollection.additionalInformationAllTables');
|
||||
} else {
|
||||
$message = sprintf($this->getLanguageService()?->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:label.tableGarbageCollection.additionalInformationTable'), $this->table);
|
||||
}
|
||||
return $message;
|
||||
}
|
||||
|
||||
public function getTaskParameters(): array
|
||||
{
|
||||
return [
|
||||
'all_tables' => $this->allTables,
|
||||
'number_of_days' => $this->numberOfDays,
|
||||
'selected_tables' => $this->table,
|
||||
];
|
||||
}
|
||||
|
||||
public function setTaskParameters(array $parameters): void
|
||||
{
|
||||
$this->allTables = (bool)($parameters['allTables'] ?? $parameters['all_tables'] ?? false);
|
||||
$this->numberOfDays = (int)($parameters['numberOfDays'] ?? $parameters['number_of_days'] ?? 0);
|
||||
$this->table = (string)($parameters['table'] ?? $parameters['selected_tables'] ?? '');
|
||||
}
|
||||
|
||||
public function getCleanableTables(array &$config): void
|
||||
{
|
||||
foreach ($this->getTableConfiguration() as $tableName => $tableConfiguration) {
|
||||
$config['items'][] = [
|
||||
'label' => $tableName . (($tableConfiguration['expirePeriod'] ?? false) ? ' [expirePeriod: ' . $tableConfiguration['expirePeriod'] . ']' : '') . (($tableConfiguration['dateField'] ?? false) ? ' [dateField: ' . $tableConfiguration['dateField'] . ']' : ''),
|
||||
'value' => $tableName,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
public function getTableConfiguration(): array
|
||||
{
|
||||
$tableConfiguration = GeneralUtility::makeInstance(TcaSchemaFactory::class)->get('tx_scheduler_task.' . self::class)->getRawConfiguration()['taskOptions']['tables'] ?? [];
|
||||
|
||||
$tableConfigurationFromConfVars = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][self::class]['options']['tables'] ?? [];
|
||||
if (!empty($tableConfigurationFromConfVars)) {
|
||||
// @deprecated will be removed in v16: this SC_OPTIONS fallback is intentionally
|
||||
// kept beyond v15 because SC_OPTIONS-based scheduler task registration
|
||||
// is still read in TaskService::getAvailableTaskTypes() to keep legacy
|
||||
// (non-native) tasks migratable. Remove together with that support.
|
||||
trigger_error('Usage of $GLOBALS[\'TYPO3_CONF_VARS\'][\'SC_OPTIONS\'][\'scheduler\'][\'tasks\'][' . self::class . '][\'options\'][\'tables\'] to define table options is deprecated and will stop working in TYPO3 v16. Use $tca[\'tx_scheduler_task\'][\'types\'][' . self::class . '][\'taskOptions\'][\'tables\'] instead.', E_USER_DEPRECATED);
|
||||
if (is_array($tableConfigurationFromConfVars)) {
|
||||
$tableConfiguration = array_replace_recursive($tableConfiguration, $tableConfigurationFromConfVars);
|
||||
}
|
||||
}
|
||||
|
||||
return $tableConfiguration;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
<?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\Task;
|
||||
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
|
||||
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Scheduler\Exception\InvalidTaskException;
|
||||
use TYPO3\CMS\Scheduler\Execution;
|
||||
use TYPO3\CMS\Scheduler\Service\TaskService;
|
||||
|
||||
/**
|
||||
* Handles serialization of `AbstractTask` objects.
|
||||
*
|
||||
* @internal This is an internal API, avoid using it in custom implementations.
|
||||
*/
|
||||
#[Autoconfigure(public: true)]
|
||||
readonly class TaskSerializer
|
||||
{
|
||||
public function __construct(
|
||||
protected ContainerInterface $container,
|
||||
protected TaskService $taskService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* This method takes care of safely deserializing tasks from the database
|
||||
* and either returns a valid Task or throws an InvalidTaskException, which
|
||||
* holds information about the broken task.
|
||||
*
|
||||
* First, find the task object, from the registry
|
||||
* Second, recreate the execution object,
|
||||
* Then fill all the data of the new task object from the rest of the row.
|
||||
*
|
||||
* @throws InvalidTaskException
|
||||
*/
|
||||
public function deserialize(array $row): AbstractTask
|
||||
{
|
||||
$taskType = $row['tasktype'] ?? '';
|
||||
if (!empty($taskType)) {
|
||||
if ($this->taskService->isTaskTypeRegistered($taskType)) {
|
||||
$taskInformation = $this->taskService->getTaskDetailsFromTaskType($taskType);
|
||||
$className = $taskInformation['className'];
|
||||
try {
|
||||
$taskObject = $this->container->get($className);
|
||||
} catch (ServiceNotFoundException) {
|
||||
$taskObject = GeneralUtility::makeInstance($className);
|
||||
}
|
||||
} else {
|
||||
throw new InvalidTaskException('Task type ' . $taskType . ' not found. Probably not registered?', 1742584362);
|
||||
}
|
||||
|
||||
if (!$taskObject instanceof AbstractTask) {
|
||||
throw new InvalidTaskException('The deserialized task in not an instance of AbstractTask', 1642954501);
|
||||
}
|
||||
if ($taskObject instanceof ExecuteSchedulableCommandTask) {
|
||||
$taskObject->setTaskType($taskType);
|
||||
}
|
||||
$taskObject->setTaskUid((int)$row['uid']);
|
||||
$taskObject->setTaskGroup((int)$row['task_group']);
|
||||
$taskParameters = json_decode($row['parameters'] ?? '', true) ?: [];
|
||||
// Set additional fields from the row with the parameters stored
|
||||
// in the parameters field for native types.
|
||||
if ($taskInformation['isNativeTask'] ?? false) {
|
||||
// If there are native registered fields, they take precedence over the values.
|
||||
foreach ($taskInformation['additionalFields'] ?? [] as $additionalFieldName) {
|
||||
$taskParameters[$additionalFieldName] = $taskParameters[$additionalFieldName] ?? $row[$additionalFieldName] ?? null;
|
||||
}
|
||||
}
|
||||
$taskObject->setTaskParameters($taskParameters);
|
||||
$taskObject->setDescription((string)$row['description']);
|
||||
$taskObject->setExecutionTime((int)$row['nextexecution']);
|
||||
$taskObject->setTaskGroup((int)$row['task_group']);
|
||||
$taskObject->setDisabled((bool)$row['disable']);
|
||||
$executionDetails = json_decode($row['execution_details'] ?? '', true);
|
||||
if ($executionDetails !== null) {
|
||||
$taskObject->setExecution(Execution::createFromDetails($executionDetails));
|
||||
}
|
||||
return $taskObject;
|
||||
}
|
||||
throw new InvalidTaskException('No task type given for task ID : ' . $row['uid'], 1740514192);
|
||||
}
|
||||
|
||||
/**
|
||||
* If the task class couldn't be figured out from the unserialization (because of uninstalled extensions or exceptions),
|
||||
* try to find it in the serialized string with a simple preg match.
|
||||
*/
|
||||
public function extractClassName(string $serializedTask): ?string
|
||||
{
|
||||
if (preg_match('/^O:[0-9]+:"(?P<classname>[^"]+)"/', $serializedTask, $matches) === 1) {
|
||||
return $matches['classname'];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?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\Task;
|
||||
|
||||
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
|
||||
|
||||
/**
|
||||
* Represents a single status flag of a scheduler task (running, late, disabled, …),
|
||||
* shared by the scheduler backend module listing and the "scheduler:list" CLI command.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final readonly class TaskStatus
|
||||
{
|
||||
/**
|
||||
* @param string $label domain:key reference of the badge label
|
||||
* @param string $message domain:key reference of the optional, more detailed message (e.g. the failure reason)
|
||||
* @param list<string|int> $messageArguments arguments for the message
|
||||
*/
|
||||
public function __construct(
|
||||
public string $type,
|
||||
public ContextualFeedbackSeverity $severity,
|
||||
public string $state,
|
||||
public string $label,
|
||||
public string $message = '',
|
||||
public array $messageArguments = [],
|
||||
) {}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?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\Validation\Validator;
|
||||
|
||||
use TYPO3\CMS\Scheduler\Task\AbstractTask;
|
||||
|
||||
class TaskValidator
|
||||
{
|
||||
/**
|
||||
* This method encapsulates a very simple test for the purpose of clarity.
|
||||
* Registered tasks are stored in the database along with a serialized task object.
|
||||
* When a registered task is fetched, its object is unserialized.
|
||||
* At that point, if the class corresponding to the object is not available anymore
|
||||
* (e.g. because the extension providing it has been uninstalled),
|
||||
* the unserialization will produce an incomplete object.
|
||||
* This test checks whether the unserialized object is of the right (parent) class or not.
|
||||
*
|
||||
* @param mixed $value The object to test
|
||||
* @return bool TRUE if object is a task, FALSE otherwise
|
||||
*/
|
||||
public function isValid(mixed $value): bool
|
||||
{
|
||||
return $value instanceof AbstractTask
|
||||
&& $value->getExecution() !== null
|
||||
&& get_class($value->getExecution()) !== \__PHP_Incomplete_Class::class;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
use TYPO3\CMS\Scheduler\Controller\NewSchedulerTaskController;
|
||||
use TYPO3\CMS\Scheduler\Controller\SchedulerModuleController;
|
||||
|
||||
/**
|
||||
* Definitions for routes provided by EXT:scheduler
|
||||
*/
|
||||
return [
|
||||
// Register new scheduler task wizard (used in a modal)
|
||||
'new_scheduler_task_wizard' => [
|
||||
'path' => '/scheduler/task/wizard/new',
|
||||
'target' => NewSchedulerTaskController::class . '::handleRequest',
|
||||
'methods' => ['GET'],
|
||||
'inheritAccessFromModule' => 'scheduler',
|
||||
],
|
||||
// Register scheduler setup check (used in a modal)
|
||||
'scheduler_setup_check' => [
|
||||
'path' => '/scheduler/setup-check',
|
||||
'target' => SchedulerModuleController::class . '::setupCheckAction',
|
||||
'methods' => ['GET'],
|
||||
'inheritAccessFromModule' => 'scheduler',
|
||||
],
|
||||
];
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
use TYPO3\CMS\Scheduler\Controller\SchedulerModuleController;
|
||||
|
||||
/**
|
||||
* Definitions for modules provided by EXT:scheduler
|
||||
*/
|
||||
return [
|
||||
'scheduler' => [
|
||||
'parent' => 'admin',
|
||||
'position' => ['after' => 'backend_user_management', 'before' => 'permissions_pages'],
|
||||
'access' => 'admin',
|
||||
'path' => '/module/scheduler',
|
||||
'workspaces' => 'live',
|
||||
'iconIdentifier' => 'module-scheduler',
|
||||
'labels' => 'scheduler.module',
|
||||
'routes' => [
|
||||
'_default' => [
|
||||
'target' => SchedulerModuleController::class . '::handleRequest',
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'dependencies' => [
|
||||
'backend',
|
||||
'core',
|
||||
],
|
||||
'imports' => [
|
||||
'@typo3/scheduler/' => 'EXT:scheduler/Resources/Public/JavaScript/',
|
||||
],
|
||||
];
|
||||
@@ -0,0 +1,8 @@
|
||||
services:
|
||||
_defaults:
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
public: false
|
||||
|
||||
TYPO3\CMS\Scheduler\:
|
||||
resource: '../Classes/*'
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
|
||||
use TYPO3\CMS\Scheduler\Task\CachingFrameworkGarbageCollectionTask;
|
||||
|
||||
defined('TYPO3') or die();
|
||||
|
||||
ExtensionManagementUtility::addTCAcolumns(
|
||||
'tx_scheduler_task',
|
||||
[
|
||||
'cache_backends' => [
|
||||
'label' => 'LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:label.cachingFrameworkGarbageCollection.selectBackends',
|
||||
'config' => [
|
||||
'type' => 'select',
|
||||
'renderType' => 'selectMultipleSideBySide',
|
||||
'itemsProcFunc' => CachingFrameworkGarbageCollectionTask::class . '->getRegisteredBackends',
|
||||
'size' => 10,
|
||||
'minitems' => 0,
|
||||
'maxitems' => 100,
|
||||
'default' => '',
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
ExtensionManagementUtility::addRecordType(
|
||||
[
|
||||
'label' => 'LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:cachingFrameworkGarbageCollection.name',
|
||||
'description' => 'LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:cachingFrameworkGarbageCollection.description',
|
||||
'value' => CachingFrameworkGarbageCollectionTask::class,
|
||||
'icon' => 'mimetypes-x-tx_scheduler_task_group',
|
||||
'group' => 'scheduler',
|
||||
],
|
||||
'
|
||||
--div--;core.form.tabs:general,
|
||||
tasktype,
|
||||
task_group,
|
||||
description,
|
||||
cache_backends;LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:label.cachingFrameworkGarbageCollection.selectBackends,
|
||||
--div--;core.form.tabs:timing,
|
||||
--palette--;;execution,
|
||||
--div--;core.form.tabs:access,
|
||||
disable,
|
||||
--div--;core.form.tabs:extended,',
|
||||
[],
|
||||
'',
|
||||
'tx_scheduler_task'
|
||||
);
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
|
||||
use TYPO3\CMS\Scheduler\Task\FileStorageExtractionTask;
|
||||
|
||||
defined('TYPO3') or die();
|
||||
|
||||
ExtensionManagementUtility::addTCAcolumns(
|
||||
'tx_scheduler_task',
|
||||
[
|
||||
'max_file_count' => [
|
||||
'label' => 'LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:label.fileStorageExtraction.fileCount',
|
||||
'config' => [
|
||||
'type' => 'number',
|
||||
'size' => 10,
|
||||
'default' => 0,
|
||||
'range' => [
|
||||
'lower' => 1,
|
||||
'upper' => 9999,
|
||||
],
|
||||
],
|
||||
],
|
||||
'registered_extractors' => [
|
||||
'config' => [
|
||||
'type' => 'none',
|
||||
'renderType' => 'registeredExtractors',
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
ExtensionManagementUtility::addRecordType(
|
||||
[
|
||||
'label' => 'LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:fileStorageExtraction.name',
|
||||
'description' => 'LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:fileStorageExtraction.description',
|
||||
'value' => FileStorageExtractionTask::class,
|
||||
'icon' => 'mimetypes-x-tx_scheduler_task_group',
|
||||
'group' => 'scheduler',
|
||||
],
|
||||
'
|
||||
--div--;core.form.tabs:general,
|
||||
tasktype,
|
||||
task_group,
|
||||
description,
|
||||
file_storage;LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:label.fileStorageIndexing.storage,
|
||||
max_file_count,
|
||||
registered_extractors,
|
||||
--div--;core.form.tabs:timing,
|
||||
--palette--;;execution,
|
||||
--div--;core.form.tabs:access,
|
||||
disable,
|
||||
--div--;core.form.tabs:extended,',
|
||||
[],
|
||||
'',
|
||||
'tx_scheduler_task'
|
||||
);
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
|
||||
use TYPO3\CMS\Scheduler\Task\FileStorageIndexingTask;
|
||||
|
||||
defined('TYPO3') or die();
|
||||
|
||||
ExtensionManagementUtility::addRecordType(
|
||||
[
|
||||
'label' => 'LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:fileStorageIndexing.name',
|
||||
'description' => 'LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:fileStorageIndexing.description',
|
||||
'value' => FileStorageIndexingTask::class,
|
||||
'icon' => 'mimetypes-x-tx_scheduler_task_group',
|
||||
'group' => 'scheduler',
|
||||
],
|
||||
'
|
||||
--div--;core.form.tabs:general,
|
||||
tasktype,
|
||||
task_group,
|
||||
description,
|
||||
file_storage;LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:label.fileStorageIndexing.storage,
|
||||
--div--;core.form.tabs:timing,
|
||||
--palette--;;execution,
|
||||
--div--;core.form.tabs:access,
|
||||
disable,
|
||||
--div--;core.form.tabs:extended,',
|
||||
[],
|
||||
'',
|
||||
'tx_scheduler_task'
|
||||
);
|
||||
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
|
||||
use TYPO3\CMS\Scheduler\Task\IpAnonymizationTask;
|
||||
|
||||
defined('TYPO3') or die();
|
||||
|
||||
ExtensionManagementUtility::addTCAcolumns(
|
||||
'tx_scheduler_task',
|
||||
[
|
||||
'ip_mask' => [
|
||||
'label' => 'LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:label.ipAnonymization.mask',
|
||||
'config' => [
|
||||
'type' => 'select',
|
||||
'renderType' => 'selectSingle',
|
||||
'default' => 2,
|
||||
'items' => [
|
||||
['label' => 'LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:label.ipAnonymization.mask.1', 'value' => 1],
|
||||
['label' => 'LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:label.ipAnonymization.mask.2', 'value' => 2],
|
||||
],
|
||||
'minitems' => 1,
|
||||
'maxitems' => 1,
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
ExtensionManagementUtility::addRecordType(
|
||||
[
|
||||
'label' => 'LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:ipAnonymization.name',
|
||||
'description' => 'LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:ipAnonymization.description',
|
||||
'value' => IpAnonymizationTask::class,
|
||||
'icon' => 'mimetypes-x-tx_scheduler_task_group',
|
||||
'group' => 'scheduler',
|
||||
],
|
||||
'
|
||||
--div--;core.form.tabs:general,
|
||||
tasktype,
|
||||
task_group,
|
||||
description,
|
||||
ip_mask,
|
||||
selected_tables;LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:label.ipAnonymization.table,
|
||||
number_of_days;LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:label.ipAnonymization.numberOfDays,
|
||||
--div--;core.form.tabs:timing,
|
||||
--palette--;;execution,
|
||||
--div--;core.form.tabs:access,
|
||||
disable,
|
||||
--div--;core.form.tabs:extended,',
|
||||
[
|
||||
'columnsOverrides' => [
|
||||
'selected_tables' => [
|
||||
'displayCond' => 'FIELD:all_tables:=:0',
|
||||
'label' => 'LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:label.ipAnonymization.table',
|
||||
'config' => [
|
||||
'type' => 'select',
|
||||
'renderType' => 'selectSingle',
|
||||
'size' => 1,
|
||||
'minitems' => 1,
|
||||
'maxitems' => 1,
|
||||
'itemsProcFunc' => IpAnonymizationTask::class . '->getAnonymizableTables',
|
||||
],
|
||||
],
|
||||
'number_of_days' => [
|
||||
'displayCond' => 'FIELD:all_tables:=:0',
|
||||
'label' => 'LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:label.ipAnonymization.numberOfDays',
|
||||
'config' => [
|
||||
'type' => 'number',
|
||||
'default' => 0,
|
||||
'range' => [
|
||||
'lower' => 0,
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'taskOptions' => [
|
||||
'tables' => [
|
||||
'sys_log' => [
|
||||
'dateField' => 'tstamp',
|
||||
'ipField' => 'IP',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'',
|
||||
'tx_scheduler_task'
|
||||
);
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
|
||||
use TYPO3\CMS\Scheduler\Task\OptimizeDatabaseTableTask;
|
||||
|
||||
defined('TYPO3') or die();
|
||||
|
||||
ExtensionManagementUtility::addRecordType(
|
||||
[
|
||||
'label' => 'LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:optimizeDatabaseTable.name',
|
||||
'description' => 'LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:optimizeDatabaseTable.description',
|
||||
'value' => OptimizeDatabaseTableTask::class,
|
||||
'icon' => 'mimetypes-x-tx_scheduler_task_group',
|
||||
'group' => 'scheduler',
|
||||
],
|
||||
'
|
||||
--div--;core.form.tabs:general,
|
||||
tasktype,
|
||||
task_group,
|
||||
description,
|
||||
selected_tables;LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:label.optimizeDatabaseTables.selectTables,
|
||||
--div--;core.form.tabs:timing,
|
||||
--palette--;;execution,
|
||||
--div--;core.form.tabs:access,
|
||||
disable,
|
||||
--div--;core.form.tabs:extended,',
|
||||
[
|
||||
'columnsOverrides' => [
|
||||
'selected_tables' => [
|
||||
'label' => 'LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:label.optimizeDatabaseTables.selectTables',
|
||||
'config' => [
|
||||
'type' => 'select',
|
||||
'renderType' => 'selectMultipleSideBySide',
|
||||
'size' => 10,
|
||||
'minitems' => 1,
|
||||
'maxitems' => 100,
|
||||
'itemsProcFunc' => OptimizeDatabaseTableTask::class . '->getOptimizableTables',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'',
|
||||
'tx_scheduler_task'
|
||||
);
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
|
||||
use TYPO3\CMS\Scheduler\Task\RecyclerGarbageCollectionTask;
|
||||
|
||||
defined('TYPO3') or die();
|
||||
|
||||
ExtensionManagementUtility::addRecordType(
|
||||
[
|
||||
'label' => 'LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:recyclerGarbageCollection.name',
|
||||
'description' => 'LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:recyclerGarbageCollection.description',
|
||||
'value' => RecyclerGarbageCollectionTask::class,
|
||||
'icon' => 'mimetypes-x-tx_scheduler_task_group',
|
||||
'group' => 'scheduler',
|
||||
],
|
||||
'
|
||||
--div--;core.form.tabs:general,
|
||||
tasktype,
|
||||
task_group,
|
||||
description,
|
||||
number_of_days;LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:label.recyclerGarbageCollection.numberOfDays,
|
||||
--div--;core.form.tabs:timing,
|
||||
--palette--;;execution,
|
||||
--div--;core.form.tabs:access,
|
||||
disable,
|
||||
--div--;core.form.tabs:extended,',
|
||||
[
|
||||
'columnsOverrides' => [
|
||||
'number_of_days' => [
|
||||
'label' => 'LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:label.recyclerGarbageCollection.numberOfDays',
|
||||
'config' => [
|
||||
'type' => 'number',
|
||||
'default' => 30,
|
||||
'range' => [
|
||||
'lower' => 0,
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'',
|
||||
'tx_scheduler_task'
|
||||
);
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
|
||||
use TYPO3\CMS\Scheduler\Task\TableGarbageCollectionTask;
|
||||
|
||||
defined('TYPO3') or die();
|
||||
|
||||
ExtensionManagementUtility::addTCAcolumns(
|
||||
'tx_scheduler_task',
|
||||
[
|
||||
'all_tables' => [
|
||||
'label' => 'LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:label.tableGarbageCollection.allTables',
|
||||
'description' => 'LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:label.tableGarbageCollection.allTables.description',
|
||||
'config' => [
|
||||
'type' => 'check',
|
||||
'renderType' => 'checkboxToggle',
|
||||
'default' => 0,
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
ExtensionManagementUtility::addRecordType(
|
||||
[
|
||||
'label' => 'LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:tableGarbageCollection.name',
|
||||
'description' => 'LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:tableGarbageCollection.description',
|
||||
'value' => TableGarbageCollectionTask::class,
|
||||
'icon' => 'mimetypes-x-tx_scheduler_task_group',
|
||||
'group' => 'scheduler',
|
||||
],
|
||||
'
|
||||
--div--;core.form.tabs:general,
|
||||
tasktype,
|
||||
task_group,
|
||||
description,
|
||||
all_tables,
|
||||
selected_tables;LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:label.tableGarbageCollection.table,
|
||||
number_of_days;LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:label.tableGarbageCollection.numberOfDays,
|
||||
--div--;core.form.tabs:timing,
|
||||
--palette--;;execution,
|
||||
--div--;core.form.tabs:access,
|
||||
disable,
|
||||
--div--;core.form.tabs:extended,',
|
||||
[
|
||||
'columnsOverrides' => [
|
||||
'selected_tables' => [
|
||||
'displayCond' => 'FIELD:all_tables:=:0',
|
||||
'label' => 'LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:label.tableGarbageCollection.table',
|
||||
'config' => [
|
||||
'type' => 'select',
|
||||
'renderType' => 'selectSingle',
|
||||
'size' => 1,
|
||||
'minitems' => 1,
|
||||
'maxitems' => 1,
|
||||
'itemsProcFunc' => TableGarbageCollectionTask::class . '->getCleanableTables',
|
||||
],
|
||||
],
|
||||
'number_of_days' => [
|
||||
'displayCond' => 'FIELD:all_tables:=:0',
|
||||
'label' => 'LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:label.tableGarbageCollection.numberOfDays',
|
||||
'config' => [
|
||||
'type' => 'number',
|
||||
'default' => 0,
|
||||
'range' => [
|
||||
'lower' => 0,
|
||||
],
|
||||
'fieldInformation' => [
|
||||
'expirePeriodInformation' => [
|
||||
'renderType' => 'expirePeriodInformation',
|
||||
'options' => [
|
||||
'refField' => 'selected_tables',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'taskOptions' => [
|
||||
'tables' => [
|
||||
'sys_log' => [
|
||||
'dateField' => 'tstamp',
|
||||
'expirePeriod' => 180,
|
||||
],
|
||||
'sys_http_report' => [
|
||||
'dateField' => 'changed',
|
||||
'expirePeriod' => 30,
|
||||
],
|
||||
'sys_history' => [
|
||||
'dateField' => 'tstamp',
|
||||
'expirePeriod' => 30,
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'',
|
||||
'tx_scheduler_task'
|
||||
);
|
||||
@@ -0,0 +1,204 @@
|
||||
<?php
|
||||
|
||||
use TYPO3\CMS\Scheduler\Service\TaskService;
|
||||
|
||||
return [
|
||||
'ctrl' => [
|
||||
'label' => 'tasktype',
|
||||
'label_alt' => 'description',
|
||||
'label_alt_force' => true,
|
||||
'title' => 'LLL:EXT:scheduler/Resources/Private/Language/locallang_tca.xlf:tx_scheduler_task',
|
||||
'crdate' => 'crdate',
|
||||
'delete' => 'deleted',
|
||||
'default_sortby' => 'nextexecution',
|
||||
'typeicon_classes' => [
|
||||
// @todo, TYPO3.icons needs to introduce tx_scheduler_task and use the current icon for "tx_scheduler_task_group"
|
||||
'default' => 'mimetypes-x-tx_scheduler_task_group',
|
||||
],
|
||||
'type' => 'tasktype',
|
||||
'hideTable' => true, // Disabled for now until sorting and grouping is usable in records module
|
||||
'adminOnly' => true, // Only admin users can edit
|
||||
'groupName' => 'system',
|
||||
'rootLevel' => 1,
|
||||
'enablecolumns' => [
|
||||
'disabled' => 'disable',
|
||||
],
|
||||
],
|
||||
'columns' => [
|
||||
'tasktype' => [
|
||||
'label' => 'LLL:EXT:scheduler/Resources/Private/Language/locallang_tca.xlf:tx_scheduler_task.tasktype',
|
||||
'config' => [
|
||||
'type' => 'select',
|
||||
'renderType' => 'taskTypeInfo',
|
||||
'itemsProcFunc' => TaskService::class . '->getTaskTypesForTcaItems',
|
||||
// Always select the first tasktype
|
||||
'items' => [],
|
||||
'default' => '',
|
||||
'required' => true,
|
||||
// relevant for migration
|
||||
'nullable' => true,
|
||||
],
|
||||
],
|
||||
'task_group' => [
|
||||
'label' => 'LLL:EXT:scheduler/Resources/Private/Language/locallang_tca.xlf:tx_scheduler_task.task_group',
|
||||
'config' => [
|
||||
'type' => 'select',
|
||||
'renderType' => 'selectSingle',
|
||||
'foreign_table' => 'tx_scheduler_task_group',
|
||||
'items' => [
|
||||
['label' => '', 'value' => 0],
|
||||
],
|
||||
'size' => 1,
|
||||
'default' => 0,
|
||||
],
|
||||
],
|
||||
'priority' => [
|
||||
'label' => 'scheduler.tca:tx_scheduler_task.priority',
|
||||
'config' => [
|
||||
'type' => 'select',
|
||||
'renderType' => 'selectSingle',
|
||||
'items' => [
|
||||
['label' => 'scheduler.tca:tx_scheduler_task.priority.high', 'value' => 150],
|
||||
['label' => 'scheduler.tca:tx_scheduler_task.priority.regular', 'value' => 100],
|
||||
['label' => 'scheduler.tca:tx_scheduler_task.priority.low', 'value' => 50],
|
||||
],
|
||||
'default' => 100,
|
||||
],
|
||||
],
|
||||
'description' => [
|
||||
'label' => 'LLL:EXT:scheduler/Resources/Private/Language/locallang_tca.xlf:tx_scheduler_task.description',
|
||||
'config' => [
|
||||
'type' => 'text',
|
||||
],
|
||||
],
|
||||
'parameters' => [
|
||||
'label' => 'LLL:EXT:scheduler/Resources/Private/Language/locallang_tca.xlf:tx_scheduler_task.parameters',
|
||||
'config' => [
|
||||
'type' => 'json',
|
||||
],
|
||||
],
|
||||
'execution_details' => [
|
||||
'label' => 'LLL:EXT:scheduler/Resources/Private/Language/locallang_tca.xlf:tx_scheduler_task.execution_details',
|
||||
'config' => [
|
||||
'type' => 'json',
|
||||
'renderType' => 'schedulerTimingOptions',
|
||||
'overrideFieldTca' => [
|
||||
'frequency' => [
|
||||
'config' => [
|
||||
'valuePicker' => [
|
||||
'items' => [
|
||||
[ 'value' => '0 9,15 * * 1-5', 'label' => 'LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:command.example1' ],
|
||||
[ 'value' => '0 */2 * * *', 'label' => 'LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:command.example2' ],
|
||||
[ 'value' => '*/20 * * * *', 'label' => 'LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:command.example3' ],
|
||||
[ 'value' => '0 7 * * 2', 'label' => 'LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:command.example4' ],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'nextexecution' => [
|
||||
'label' => 'LLL:EXT:scheduler/Resources/Private/Language/locallang_tca.xlf:tx_scheduler_task.nextexecution',
|
||||
'config' => [
|
||||
'type' => 'datetime',
|
||||
'readOnly' => true,
|
||||
],
|
||||
],
|
||||
'lastexecution_time' => [
|
||||
'label' => 'LLL:EXT:scheduler/Resources/Private/Language/locallang_tca.xlf:tx_scheduler_task.lastexecution_time',
|
||||
'config' => [
|
||||
'type' => 'datetime',
|
||||
'readOnly' => true,
|
||||
],
|
||||
],
|
||||
'lastexecution_failure' => [
|
||||
'label' => 'LLL:EXT:scheduler/Resources/Private/Language/locallang_tca.xlf:tx_scheduler_task.lastexecution_failure',
|
||||
'config' => [
|
||||
'type' => 'text',
|
||||
'readOnly' => true,
|
||||
],
|
||||
],
|
||||
'lastexecution_context' => [
|
||||
'label' => 'LLL:EXT:scheduler/Resources/Private/Language/locallang_tca.xlf:tx_scheduler_task.lastexecution_context',
|
||||
'config' => [
|
||||
'type' => 'select',
|
||||
'renderType' => 'selectSingle',
|
||||
'items' => [
|
||||
['value' => 'CLI', 'label' => 'CLI'],
|
||||
['value' => 'BE', 'label' => 'BE'],
|
||||
['value' => '', 'label' => ''],
|
||||
],
|
||||
'readOnly' => true,
|
||||
'dbFieldLength' => 3,
|
||||
'default' => '',
|
||||
],
|
||||
],
|
||||
'serialized_executions' => [
|
||||
'config' => [
|
||||
'type' => 'passthrough',
|
||||
],
|
||||
],
|
||||
'number_of_days' => [
|
||||
'label' => 'LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:tx_scheduler_task.number_of_days',
|
||||
'config' => [
|
||||
'type' => 'number',
|
||||
'default' => 0,
|
||||
],
|
||||
],
|
||||
'selected_tables' => [
|
||||
'label' => 'LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:tx_scheduler_task.selected_tables',
|
||||
'config' => [
|
||||
'type' => 'select',
|
||||
'renderType' => 'selectMultipleSideBySide',
|
||||
'size' => 10,
|
||||
'dbFieldLength' => 4000,
|
||||
'minitems' => 1,
|
||||
'maxitems' => 100,
|
||||
'items' => [],
|
||||
],
|
||||
],
|
||||
'file_storage' => [
|
||||
'label' => 'LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:tx_scheduler_task.file_storage',
|
||||
'config' => [
|
||||
'type' => 'select',
|
||||
'renderType' => 'selectSingle',
|
||||
'foreign_table' => 'sys_file_storage',
|
||||
'size' => 1,
|
||||
'minitems' => 1,
|
||||
'maxitems' => 1,
|
||||
'items' => [],
|
||||
],
|
||||
],
|
||||
],
|
||||
'types' => [
|
||||
'0' => [
|
||||
'showitem' => '
|
||||
--div--;core.form.tabs:general,
|
||||
tasktype,
|
||||
task_group,
|
||||
description,
|
||||
--div--;core.form.tabs:timing,
|
||||
--palette--;;execution,
|
||||
--div--;core.form.tabs:access,
|
||||
disable,
|
||||
--div--;core.form.tabs:extended,
|
||||
',
|
||||
],
|
||||
],
|
||||
'palettes' => [
|
||||
'execution' => [
|
||||
'showitem' => '
|
||||
execution_details,
|
||||
--linebreak--,
|
||||
nextexecution,
|
||||
priority,
|
||||
--linebreak--,
|
||||
lastexecution_context,
|
||||
lastexecution_time,
|
||||
--linebreak--,
|
||||
lastexecution_failure,
|
||||
',
|
||||
],
|
||||
],
|
||||
];
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'ctrl' => [
|
||||
'label' => 'groupName',
|
||||
'tstamp' => 'tstamp',
|
||||
'title' => 'LLL:EXT:scheduler/Resources/Private/Language/locallang_tca.xlf:tx_scheduler_task_group',
|
||||
'crdate' => 'crdate',
|
||||
'delete' => 'deleted',
|
||||
'sortby' => 'sorting',
|
||||
'typeicon_classes' => [
|
||||
'default' => 'mimetypes-x-tx_scheduler_task_group',
|
||||
],
|
||||
'adminOnly' => true, // Only admin users can edit
|
||||
'groupName' => 'system',
|
||||
'rootLevel' => 1,
|
||||
'enablecolumns' => [
|
||||
'disabled' => 'hidden',
|
||||
],
|
||||
],
|
||||
'columns' => [
|
||||
'groupName' => [
|
||||
'label' => 'LLL:EXT:scheduler/Resources/Private/Language/locallang_tca.xlf:tx_scheduler_task_group.groupName',
|
||||
'config' => [
|
||||
'type' => 'input',
|
||||
'size' => 35,
|
||||
'max' => 80,
|
||||
'required' => true,
|
||||
'eval' => 'unique,trim',
|
||||
'softref' => 'substitute',
|
||||
],
|
||||
],
|
||||
'color' => [
|
||||
'label' => 'LLL:EXT:scheduler/Resources/Private/Language/locallang_tca.xlf:tx_scheduler_task_group.color',
|
||||
'config' => [
|
||||
'type' => 'color',
|
||||
'size' => 10,
|
||||
'valuePicker' => [
|
||||
'items' => [
|
||||
['label' => 'LLL:EXT:scheduler/Resources/Private/Language/locallang_tca.xlf:tx_scheduler_task_group.color.typo3Orange', 'value' => '#FF8700'],
|
||||
['label' => 'LLL:EXT:scheduler/Resources/Private/Language/locallang_tca.xlf:tx_scheduler_task_group.color.white', 'value' => '#ffffff'],
|
||||
['label' => 'LLL:EXT:scheduler/Resources/Private/Language/locallang_tca.xlf:tx_scheduler_task_group.color.gray', 'value' => '#808080'],
|
||||
['label' => 'LLL:EXT:scheduler/Resources/Private/Language/locallang_tca.xlf:tx_scheduler_task_group.color.black', 'value' => '#000000'],
|
||||
['label' => 'LLL:EXT:scheduler/Resources/Private/Language/locallang_tca.xlf:tx_scheduler_task_group.color.blue', 'value' => '#2671d9'],
|
||||
['label' => 'LLL:EXT:scheduler/Resources/Private/Language/locallang_tca.xlf:tx_scheduler_task_group.color.purple', 'value' => '#5e4db2'],
|
||||
['label' => 'LLL:EXT:scheduler/Resources/Private/Language/locallang_tca.xlf:tx_scheduler_task_group.color.teal', 'value' => '#2da8d2'],
|
||||
['label' => 'LLL:EXT:scheduler/Resources/Private/Language/locallang_tca.xlf:tx_scheduler_task_group.color.green', 'value' => '#3cc38c'],
|
||||
['label' => 'LLL:EXT:scheduler/Resources/Private/Language/locallang_tca.xlf:tx_scheduler_task_group.color.magenta', 'value' => '#c6398f'],
|
||||
['label' => 'LLL:EXT:scheduler/Resources/Private/Language/locallang_tca.xlf:tx_scheduler_task_group.color.yellow', 'value' => '#ffbf00'],
|
||||
['label' => 'LLL:EXT:scheduler/Resources/Private/Language/locallang_tca.xlf:tx_scheduler_task_group.color.red', 'value' => '#d13a2e'],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'description' => [
|
||||
'label' => 'LLL:EXT:scheduler/Resources/Private/Language/locallang_tca.xlf:tx_scheduler_task_group.description',
|
||||
'config' => [
|
||||
'type' => 'text',
|
||||
],
|
||||
],
|
||||
],
|
||||
'types' => [
|
||||
'1' => [
|
||||
'showitem' => '
|
||||
--div--;core.form.tabs:general, groupName, color,
|
||||
--div--;core.form.tabs:access, hidden,
|
||||
--div--;core.form.tabs:notes, description,
|
||||
--div--;core.form.tabs:extended,
|
||||
',
|
||||
],
|
||||
],
|
||||
];
|
||||
@@ -0,0 +1 @@
|
||||
options.showDuplicate.tx_scheduler_task = 0
|
||||
@@ -0,0 +1,120 @@
|
||||
:navigation-title: Backend Module
|
||||
|
||||
.. include:: /Includes.rst.txt
|
||||
.. _backend-module:
|
||||
|
||||
==============================
|
||||
The "Scheduler" backend module
|
||||
==============================
|
||||
|
||||
.. contents:: Table of contents
|
||||
|
||||
.. _setup-check-screen:
|
||||
|
||||
The setup check screen
|
||||
======================
|
||||
|
||||
This screen has already been mentioned in the :ref:`Installation chapter <setup-check>`.
|
||||
It is mostly useful when setting up the Scheduler, as it indicates
|
||||
whether the CLI script is executable or not. When everything is
|
||||
running fine, it contains mostly one useful piece of information: when
|
||||
the last run took place, when it ended and whether it was started
|
||||
manually (i.e. from the BE module) or automatically (i.e. from the
|
||||
command line).
|
||||
|
||||
|
||||
.. _scheduled-tasks-screen:
|
||||
|
||||
The scheduled tasks screen
|
||||
==========================
|
||||
|
||||
This is the main screen when administering tasks. At first it will be
|
||||
empty and just offer a link to add a new task. When such registered
|
||||
tasks exists, this screen will show a list with various pieces of
|
||||
information.
|
||||
|
||||
.. figure:: /Images/BackendModuleMainView.png
|
||||
:alt: Scheduler main screen
|
||||
|
||||
Main screen of the Scheduler BE module
|
||||
|
||||
Disabled tasks have a gray label sign near the task name. A disabled task is a
|
||||
task that will not be run automatically by the command-line script,
|
||||
but may still be executed from the BE module.
|
||||
|
||||
A late task will appear with an orange label sign near the task name:
|
||||
|
||||
.. figure:: /Images/LateTask.png
|
||||
:alt: A late task in the Scheduler main screen
|
||||
|
||||
A late task in the main screen of the Scheduler BE module
|
||||
|
||||
|
||||
The task list can be sorted by clicking the column label. With every click it
|
||||
switches between ascending and descending order of the items of the associated column.
|
||||
|
||||
The table at the center of the above screenshot shows the
|
||||
following:
|
||||
|
||||
- The first column contains checkboxes. Clicking on a checkbox will
|
||||
select that particular scheduled task for immediate execution.
|
||||
Clicking on the icon at the top of the column will toggle all
|
||||
checkboxes. To execute the selected tasks, click on the "Execute
|
||||
selected tasks" button. Read more in "Manually executing a task"
|
||||
below.
|
||||
|
||||
- The second column simply displays the id of the task.
|
||||
|
||||
- The third column contains the name of the task, the extension it is
|
||||
coming from and any additional information specific to the task. It also shows
|
||||
a summary of the task's status with an icon.
|
||||
|
||||
- The fourth column shows whether the task is recurring or will run only
|
||||
a single time.
|
||||
|
||||
- The fifth column shows the frequency.
|
||||
|
||||
- The sixth columns indicates whether parallel executions are allowed
|
||||
or not.
|
||||
|
||||
- The seventh column shows the last execution time and indicates whether
|
||||
the task was launched manually or was run via the command-line script
|
||||
(cron).
|
||||
|
||||
- The eighth column shows the planned execution time. If the task is
|
||||
overdue, the time will show up in bold, red numbers. A task may have
|
||||
no future execution date if it has reached its end date, if it was
|
||||
meant to run a single time and that execution is done, or if the task
|
||||
is disabled. The next execution time is also hidden for running tasks,
|
||||
as this information makes no sense at that point in time.
|
||||
|
||||
- The last column contains possible actions, mainly editing, disable or
|
||||
deleting a task. There are also buttons for running the task on the
|
||||
next cron job or run it directly.
|
||||
The actions will be unavailable for a task that is currently running,
|
||||
as it is unwise to edit or delete it a task in such a case. Instead a
|
||||
running task will display a "stop" button (see "Stopping a task" below).
|
||||
|
||||
|
||||
|
||||
Note that all dates and times are displayed in the server's time zone.
|
||||
The server time appears at the bottom of the screen.
|
||||
|
||||
At the top of the screen is a link to add a new task.
|
||||
If there are a lot of tasks that appear late,
|
||||
consider changing the frequency at which the cron job is running (see
|
||||
"Choosing a frequency" above).
|
||||
|
||||
Occasionally the following display may appear:
|
||||
|
||||
.. figure:: ../../Images/MissingTaskClass.png
|
||||
:alt: A broken task
|
||||
|
||||
A scheduled task missing its corresponding class
|
||||
|
||||
This will typically happen when a task provided by some
|
||||
extension was registered, then the extension was uninstalled, but the
|
||||
task was not deleted beforehand. In such a case, this task stays but
|
||||
the Scheduler doesn't know how to handle it anymore. The solution is
|
||||
either to install the related extension again or delete the registered
|
||||
task.
|
||||
@@ -0,0 +1,119 @@
|
||||
:navigation-title: Console tools
|
||||
|
||||
.. include:: /Includes.rst.txt
|
||||
.. _console-tools:
|
||||
|
||||
=======================================
|
||||
Console tools to manage scheduler tasks
|
||||
=======================================
|
||||
|
||||
Console commands to manage scheduler tasks include :command:`typo3 scheduler:list`,
|
||||
:command:`typo3 scheduler:execute` and :command:`typo3 scheduler:run`.
|
||||
|
||||
You can display detailed help on these commands, by using the `--help` parameter to
|
||||
display the help:
|
||||
|
||||
.. tabs::
|
||||
|
||||
.. group-tab:: Composer mode
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
vendor/bin/typo3 scheduler:list --help
|
||||
|
||||
.. group-tab:: Classic mode
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
typo3/sysext/core/bin/typo3 scheduler:list --help
|
||||
|
||||
See also: `Command usage in terminal environments <https://docs.typo3.org/permalink/t3coreapi:how-to-run-a-command>`_.
|
||||
|
||||
.. contents:: Table of contents
|
||||
|
||||
.. toctree::
|
||||
:glob:
|
||||
:caption: Subpages
|
||||
:titlesonly:
|
||||
|
||||
*
|
||||
|
||||
.. _console-run:
|
||||
|
||||
Running the scheduler
|
||||
=====================
|
||||
|
||||
The command :command:`typo3 scheduler:run` is usually called by the
|
||||
`cron job <https://docs.typo3.org/permalink/typo3/cms-scheduler:cron-job>`_.
|
||||
|
||||
It looks for tasks that are **due**, and runs them. You can
|
||||
optionally target specific task IDs, force them even if not due, or stop them.
|
||||
|
||||
.. tabs::
|
||||
|
||||
.. group-tab:: Composer mode
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
vendor/bin/typo3 scheduler:run
|
||||
|
||||
.. group-tab:: Classic mode
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
typo3/sysext/core/bin/typo3 scheduler:run
|
||||
|
||||
.. seealso::
|
||||
`Running the scheduler: typo3 scheduler:run <https://docs.typo3.org/permalink/typo3/cms-scheduler:scheduler-shell-script>`_
|
||||
|
||||
.. _console-execute:
|
||||
|
||||
Executing scheduler tasks
|
||||
=========================
|
||||
|
||||
The command :command:`typo3 scheduler:execute` is a "manual fire" runner. You pick
|
||||
tasks (IDs or whole groups) and it **executes them on demand**, regardless of
|
||||
whether they are due. It can also prompt you interactively to choose.
|
||||
|
||||
.. tabs::
|
||||
|
||||
.. group-tab:: Composer mode
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
# Note the id of the task
|
||||
vendor/bin/typo3 scheduler:list
|
||||
|
||||
vendor/bin/typo3 scheduler:execute --task=<taskUid>
|
||||
|
||||
.. group-tab:: Classic mode
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
# Find the id of the task
|
||||
typo3/sysext/core/bin/typo3 scheduler:list
|
||||
|
||||
typo3/sysext/core/bin/typo3 scheduler:execute --task=<taskUid>
|
||||
|
||||
.. _console-list:
|
||||
|
||||
Listing all scheduler tasks
|
||||
===========================
|
||||
|
||||
Command :command:`typo3 scheduler:list` can be used to list all available tasks.
|
||||
This command basically displays the same information as the backend module
|
||||
:guilabel:`Administration > Scheduler`.
|
||||
|
||||
.. tabs::
|
||||
|
||||
.. group-tab:: Composer mode
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
vendor/bin/typo3 scheduler:list
|
||||
|
||||
.. group-tab:: Classic mode
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
typo3/sysext/core/bin/typo3 scheduler:list
|
||||
@@ -0,0 +1,141 @@
|
||||
:navigation-title: Run the scheduler
|
||||
|
||||
.. include:: /Includes.rst.txt
|
||||
.. _scheduler-shell-script:
|
||||
|
||||
==========================================
|
||||
Running the scheduler: typo3 scheduler:run
|
||||
==========================================
|
||||
|
||||
The scheduler provides a PHP shell script designed to be run using
|
||||
TYPO3's command-line dispatcher. To try and run that script a first
|
||||
time, type the following command.
|
||||
|
||||
.. tabs::
|
||||
|
||||
.. group-tab:: Composer-based installation
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
vendor/bin/typo3 scheduler:run
|
||||
|
||||
.. group-tab:: Classic installation
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
typo3/sysext/core/bin/typo3 scheduler:run
|
||||
|
||||
See also `TYPO3 Explained: Run a command from the command
|
||||
line <https://docs.typo3.org/permalink/t3coreapi:symfony-console-commands-cli>`_.
|
||||
|
||||
.. contents:: Table of contents
|
||||
|
||||
.. _scheduler-shell-script-help:
|
||||
|
||||
Show help
|
||||
=========
|
||||
|
||||
In order to show help:
|
||||
|
||||
.. tabs::
|
||||
|
||||
.. group-tab:: Composer-based installation
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
vendor/bin/typo3 scheduler:run --help
|
||||
|
||||
.. group-tab:: Classic installation
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
typo3/sysext/core/bin/typo3 scheduler:run --help
|
||||
|
||||
.. _scheduler-shell-script-options:
|
||||
|
||||
Providing options to the shell script
|
||||
=====================================
|
||||
|
||||
The shell scripts accepts a number of options which can be provided in any
|
||||
order.
|
||||
|
||||
.. _scheduler-shell-script-options-i:
|
||||
|
||||
`--task (-i)`
|
||||
-------------
|
||||
|
||||
To run a specific scheduler task you need to provide the uid of the task:
|
||||
|
||||
.. tabs::
|
||||
|
||||
.. group-tab:: Composer-based installation
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
# Run task with uid 42
|
||||
vendor/bin/typo3 scheduler:run --task=42
|
||||
|
||||
# Run tasks with uid 3 and 14
|
||||
vendor/bin/typo3 scheduler:run --task=3 --task=14
|
||||
|
||||
.. group-tab:: Classic installation
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
# Run task with uid 42
|
||||
typo3/sysext/core/bin/typo3 scheduler:run --task=42
|
||||
|
||||
# Run tasks with uid 3 and 14
|
||||
typo3/sysext/core/bin/typo3 scheduler:run --task=3 --task=14
|
||||
|
||||
The tasks will be executed in the order in which the parameters are provided.
|
||||
|
||||
.. _scheduler-shell-script-options-f:
|
||||
|
||||
`--force (-f)`
|
||||
--------------
|
||||
|
||||
To run a task even if it is disabled (or not scheduled to be run yet),
|
||||
you need to provide the force option:
|
||||
|
||||
.. tabs::
|
||||
|
||||
.. group-tab:: Composer-based installation
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
# Run task with uid 42, even if disabled
|
||||
vendor/bin/typo3 scheduler:run --task=42 --force
|
||||
|
||||
.. group-tab:: Classic installation
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
# Run task with uid 42, even if disabled
|
||||
typo3/sysext/core/bin/typo3 scheduler:run --task=42 --force
|
||||
|
||||
This will also run the task with uid 42 if it is disabled.
|
||||
|
||||
.. _scheduler-shell-script-options-v:
|
||||
|
||||
`--verbose (-v)`
|
||||
----------------
|
||||
|
||||
A single `-v` flag will output errors only. Two `-vv` flags will also output
|
||||
additional information:
|
||||
|
||||
.. tabs::
|
||||
|
||||
.. group-tab:: Composer-based installation
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
# Run task with uid 42, with detailed stack traces
|
||||
vendor/bin/typo3 scheduler:run --task=42 -vv
|
||||
|
||||
.. group-tab:: Classic installation
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
# Run task with uid 42, with detailed stack traces
|
||||
typo3/sysext/core/bin/typo3 scheduler:run --task=42 -vv
|
||||
@@ -0,0 +1,34 @@
|
||||
.. include:: /Includes.rst.txt
|
||||
|
||||
.. _deleting-a-task:
|
||||
|
||||
===============
|
||||
Deleting a task
|
||||
===============
|
||||
|
||||
When choosing to delete a task, a pop-up window will appear requesting
|
||||
confirmation.
|
||||
|
||||
Deleted tasks can be recovered in the module :guilabel:`Content > Recycler` if
|
||||
installed or by unsetting the deleted flag in the database.
|
||||
|
||||
.. _deleting-a-task-restoration:
|
||||
|
||||
Restoring a deleted scheduler task
|
||||
==================================
|
||||
|
||||
.. versionchanged:: 14.0
|
||||
Previously removed tasks can be restored via recycler.
|
||||
|
||||
If the system extension :composer:`typo3/cms-recycler` is installed, go to
|
||||
module :guilabel:`Content > Recycler`.
|
||||
|
||||
Choose the page root (page 0) in the page tree, all scheduler tasks are stored
|
||||
here.
|
||||
|
||||
You can use the field "Type" to filter for "Scheduler task" only.
|
||||
|
||||
.. figure:: /Images/RestoreTask.png
|
||||
:alt: Screenshot of the TYPO3 Backend Recycler module on the root page 0 with type "Scheduler task" selected
|
||||
|
||||
A task can be restored or permanently deleted in the recycler module
|
||||
@@ -0,0 +1,104 @@
|
||||
:navigation-title: Adding / Editing
|
||||
|
||||
.. include:: /Includes.rst.txt
|
||||
.. _adding-editing-task:
|
||||
|
||||
========================
|
||||
Adding or editing a task
|
||||
========================
|
||||
|
||||
Administrators can add or edit scheduler tasks in the backend module
|
||||
:guilabel:`Administration > Scheduler`.
|
||||
|
||||
When adding a new scheduler task a wizard will allow you to select a task
|
||||
type from several categories.
|
||||
|
||||
.. seealso::
|
||||
Developers can implement and register custom tasks:
|
||||
`Creating a custom scheduler task <https://docs.typo3.org/permalink/typo3/cms-scheduler:creating-tasks>`_
|
||||
|
||||
.. contents:: Table of contents
|
||||
|
||||
.. _information-screen:
|
||||
.. _adding-editing-task-wizard:
|
||||
|
||||
The scheduler task wizard
|
||||
=========================
|
||||
|
||||
.. figure:: /Images/EmptySchedulerModule.png
|
||||
:alt: Screenshot of an empty scheduler module, No tasks defined yet.
|
||||
|
||||
Click on "New task" to add a task
|
||||
|
||||
.. figure:: /Images/TaskCreationWizard.png
|
||||
:alt: Screenshot of the "New task" wizard in the scheduler backend module
|
||||
|
||||
Choose the task to be created
|
||||
|
||||
.. seealso::
|
||||
Developers can listen to event `ModifyNewSchedulerTaskWizardItemsEvent <https://docs.typo3.org/permalink/typo3/cms-scheduler:modifynewschedulertaskwizarditemsevent>`_
|
||||
to influence the items displayed here.
|
||||
|
||||
.. _adding-editing-task-form:
|
||||
|
||||
The scheduler task form
|
||||
=======================
|
||||
|
||||
When adding or editing a task, the following form will show up:
|
||||
|
||||
.. figure:: /Images/AddingATask.png
|
||||
:alt: Screenshot of the form to Create new Scheduler task on root level
|
||||
|
||||
Adding a new scheduled task
|
||||
|
||||
Some fields require additional explanations (inline help is
|
||||
available by moving the mouse over the field labels):
|
||||
|
||||
- A disabled task will be skipped by the command-line script. It may
|
||||
still be launched manually, as described above.
|
||||
|
||||
.. versionadded:: 13.3
|
||||
Similar to editing regular content elements, it is now possible to save
|
||||
scheduler tasks being edited via keyboard shortcuts as well.
|
||||
|
||||
It is possible to invoke the :kbd:`Ctrl`/:kbd:`Cmd` + :kbd:`s` hotkey to save a
|
||||
scheduler task, altogether with the hotkey :kbd:`Ctrl`/:kbd:`Cmd` + :kbd:`Shift` + :kbd:`S`
|
||||
to save and close a scheduler task.
|
||||
|
||||
.. _adding-editing-task-form-settings:
|
||||
|
||||
Scheduler task settings
|
||||
=======================
|
||||
|
||||
Some tasks allow additional settings to be made in the area :guilabel:`Settings`.
|
||||
These fields differ from task to task
|
||||
|
||||
.. _adding-editing-task-form-timing:
|
||||
|
||||
Task executions timing details
|
||||
==============================
|
||||
|
||||
.. figure:: /Images/TaskExecutionDetails.png
|
||||
:alt: Screenshot of tab "Timing" the scheduler task form
|
||||
|
||||
Choosing a frequency for a recurring task
|
||||
|
||||
- A task must have a start date. It defaults to the time of creation.
|
||||
The server's time appears at the bottom of the form.
|
||||
|
||||
- Task can be run a single time or recurring.
|
||||
|
||||
- The frequency needs be entered only for recurring tasks.
|
||||
It can be either an integer number of seconds or a cron-like schedule expression.
|
||||
Scheduler supports ranges, steps and keywords like ``@weekly``.
|
||||
See `en.wikipedia.org <https://en.wikipedia.org/wiki/Cron#CRON_expression>`_ for more information.
|
||||
See :php:`\TYPO3\CMS\Scheduler\CronCommand\CronCommand`
|
||||
and :php:`\TYPO3\CMS\Scheduler\CronCommand\NormalizeCommand`
|
||||
class references in the TYPO3 CMS source code for definitive rules.
|
||||
|
||||
- Parallel executions are denied for recurring tasks. They can be allowed by
|
||||
checking "Allow Parallel Execution"
|
||||
|
||||
If an error occurs when validating a cron definition, the
|
||||
Scheduler's built-in cron parser tries to provide an explanation about
|
||||
what's wrong.
|
||||
@@ -0,0 +1,69 @@
|
||||
:navigation-title: Grouping
|
||||
|
||||
.. include:: /Includes.rst.txt
|
||||
.. _grouping-tasks:
|
||||
|
||||
=======================================================
|
||||
Grouping tasks together in the Scheduler backend module
|
||||
=======================================================
|
||||
|
||||
In case of a high number of different tasks, it may be useful to visually
|
||||
group similar tasks together:
|
||||
|
||||
.. figure:: /Images/GroupedTasks.png
|
||||
:alt: Screenshot of the TYPO3 backend module scheduler with buttons regarding groups highlighted
|
||||
|
||||
Use button :guilabel:`New group` (1) to create a group, click on the title (2) to edit the group
|
||||
|
||||
You can sort and disable groups (3).
|
||||
|
||||
Unused groups are displayed at the bottom of the page. Only unused groups can
|
||||
be deleted (4).
|
||||
|
||||
When editing a group you can change its title and color.
|
||||
|
||||
.. figure:: /Images/GroupEdit.png
|
||||
:alt: Screenshot of the task group form
|
||||
|
||||
Color options can be customized by manipulating the
|
||||
:php:`$GLOBAS['TCA']['tx_scheduler_task_group']['columns']['color']['config']['valuePicker']['items']`
|
||||
array in your TCA overrides file.
|
||||
|
||||
.. code-block:: php
|
||||
:caption: packages/my-sitepackage/Configuration/Overrides/tx_scheduler_task_group.php
|
||||
|
||||
$GLOBALS['TCA']['tx_scheduler_task_group']['columns']['color']['config']['valuePicker']['items'][] = [
|
||||
'label' => 'My Color',
|
||||
'value' => '#ABCDEF'
|
||||
];
|
||||
|
||||
.. _grouping-tasks-edit:
|
||||
|
||||
Editing task groups
|
||||
===================
|
||||
|
||||
Scheduler task groups can be created, edited and deleted from the module
|
||||
:guilabel:`Administration > Scheduler`.
|
||||
|
||||
Technically they are records stored on the root page (pid=0). They can also be
|
||||
created, edited and sorted with module :guilabel:`Content > Records`.
|
||||
|
||||
It is also possible to create a new task group from within the edit task form by
|
||||
clicking on the `+` icon next to the task group select box.
|
||||
|
||||
.. _grouping-tasks-disable:
|
||||
|
||||
Disabling task groups
|
||||
=====================
|
||||
|
||||
You can use button :guilabel:`Disable group` to disable all tasks in a group
|
||||
at once.
|
||||
|
||||
.. figure:: /Images/GroupDisabled.png
|
||||
:alt: Screenshot a disabled task group, all tasks are marked as disabled by group
|
||||
|
||||
Use button :guilabel:`Enable group` to enable all tasks that had not been manually disabled.
|
||||
|
||||
Tasks in a disabled group, just like disabled tasks in general are not executed
|
||||
when the scheduler is called by the cron job. They can, however, be executed
|
||||
manually by clicking the :guilabel:`Run task` button.
|
||||
@@ -0,0 +1,25 @@
|
||||
:navigation-title: Administration
|
||||
|
||||
.. include:: /Includes.rst.txt
|
||||
.. _administration:
|
||||
|
||||
=================================================
|
||||
Backend administration in the "Scheduler" module
|
||||
=================================================
|
||||
|
||||
The Scheduler provides a BE module to manage tasks. It provides three
|
||||
screens: a setup check, an information screen and the main (default)
|
||||
one for actually managing the tasks.
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 5
|
||||
:titlesonly:
|
||||
:glob:
|
||||
|
||||
BackendModule/Index
|
||||
EditTask/Index
|
||||
DeleteTask/Index
|
||||
GroupTask/Index
|
||||
StopTask/Index
|
||||
ManualExecution/Index
|
||||
ConsoleTools/Index
|
||||
@@ -0,0 +1,56 @@
|
||||
:navigation-title: Manual Execution
|
||||
|
||||
.. include:: /Includes.rst.txt
|
||||
.. _manually-executing-a-task:
|
||||
.. _executing-a-task-on-next-cronjob:
|
||||
|
||||
===========================================================
|
||||
Manually executing a task from the Scheduler backend module
|
||||
===========================================================
|
||||
|
||||
You can manually execute tasks from the BE module. After execution, each
|
||||
task shows success or failure.
|
||||
|
||||
* If a task was overdue, a new execution date is calculated.
|
||||
* If it was not overdue, the existing next execution date remains.
|
||||
|
||||
Running tasks:
|
||||
|
||||
* To run a single task, press the button in its row.
|
||||
* To run multiple tasks, select their checkboxes and press the button below the list.
|
||||
|
||||
There are two options:
|
||||
|
||||
* Run the task immediately. (Button 2 in the screenshot)
|
||||
* Schedule the task. (Button 1 in the screenshot) The selected tasks will
|
||||
then run on the next cron job.
|
||||
|
||||
.. figure:: /Images/ManualExecution.png
|
||||
:alt: Scheduler backend module with the buttons "Run task on next cron job" (1) and "Run task" (2) highlighted
|
||||
|
||||
Button 2 runs the task immediately, while button 1 schedules it fot the next cronjob run
|
||||
|
||||
.. _manually-executing-a-task-cli:
|
||||
|
||||
Manually executing a task from the console
|
||||
==========================================
|
||||
|
||||
.. tabs::
|
||||
|
||||
.. group-tab:: Composer mode
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
# Note the id of the task
|
||||
vendor/bin/typo3 scheduler:list
|
||||
|
||||
vendor/bin/typo3 scheduler:execute --task=<taskUid>
|
||||
|
||||
.. group-tab:: Classic mode
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
# Find the id of the task
|
||||
typo3/sysext/core/bin/typo3 scheduler:list
|
||||
|
||||
typo3/sysext/core/bin/typo3 scheduler:execute --task=<taskUid>
|
||||
@@ -0,0 +1,92 @@
|
||||
:navigation-title: Stopping a Task
|
||||
|
||||
.. include:: /Includes.rst.txt
|
||||
.. _stopping-a-task:
|
||||
|
||||
===============================================
|
||||
Stopping a task in the Scheduler backend module
|
||||
===============================================
|
||||
|
||||
A task is marked as "running" while it runs. If the process crashes or
|
||||
is killed, the task may remain marked as "running". This is usually
|
||||
cleaned up automatically based on the maximum lifetime parameter,
|
||||
but manual cleanup may sometimes be needed.
|
||||
|
||||
.. figure:: /Images/StoppingATask.png
|
||||
:alt: Stopping a task
|
||||
|
||||
Stopping a running task from the main screen
|
||||
|
||||
Use the **stop** button to clear the execution mark for a task.
|
||||
This allows the task to run again.
|
||||
|
||||
Note: This does **not** terminate an actual running or hanging process.
|
||||
|
||||
.. _stopping-a-task-cli:
|
||||
|
||||
Stopping a task via console command
|
||||
===================================
|
||||
|
||||
You can also use a command to stop the task:
|
||||
|
||||
.. tabs::
|
||||
|
||||
.. group-tab:: Composer mode
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
# Note the id of the task
|
||||
vendor/bin/typo3 scheduler:list
|
||||
|
||||
vendor/bin/typo3 scheduler:run --task=<taskUid> --stop
|
||||
|
||||
.. group-tab:: Classic mode
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
# Find the id of the task
|
||||
typo3/sysext/core/bin/typo3 scheduler:list
|
||||
|
||||
typo3/sysext/core/bin/typo3 scheduler:run --task=<taskUid> --stop
|
||||
|
||||
.. _kill-task:
|
||||
|
||||
How to handle a truly "hung" task
|
||||
=================================
|
||||
|
||||
If a task hangs or is stuck (for example due to an infinite loop or external I/O),
|
||||
then stopping it via TYPO3 (either UI or CLI) will only clear TYPO3’s internal flag.
|
||||
|
||||
The actual PHP process running the task will continue on the system
|
||||
until it finishes or the OS/PHP process manager kills it.
|
||||
|
||||
If a task keeps running indefinitely:
|
||||
|
||||
#. **Identify the process PID** You can find the hanging PHP process using
|
||||
tools like `top`, `ps aux | grep scheduler`, or via `systemctl status` if you run TYPO3 via a
|
||||
systemd service.
|
||||
#. **Manually terminate the PHP process** (for example, `kill <pid>`).
|
||||
#. **Clear the TYPO3 execution flag** by running:
|
||||
:command:`vendor/bin/typo3 scheduler:run --task=<taskUid> --stop`
|
||||
|
||||
.. warning::
|
||||
Manually terminating a scheduler process using `sudo kill <pid>` should only
|
||||
be used as a *last resort*.
|
||||
|
||||
Killing a running PHP process may interrupt database or file operations and
|
||||
leave the system in an inconsistent state.
|
||||
|
||||
Always analyze why the task is hanging before killing it.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
ps aux | grep scheduler
|
||||
|
||||
# you might find something like this:
|
||||
www-data 12345 99.0 5.2 php vendor/bin/typo3 scheduler:run
|
||||
|
||||
# To force-stop that OS-level process:
|
||||
sudo kill 12345
|
||||
|
||||
# Then clear the mark again via:
|
||||
vendor/bin/typo3 scheduler:run --task=13 --stop
|
||||
@@ -0,0 +1,94 @@
|
||||
:navigation-title: Table Garbage Collection
|
||||
|
||||
.. include:: /Includes.rst.txt
|
||||
|
||||
.. _table-garbage-collection-task:
|
||||
|
||||
=============================
|
||||
Table garbage collection task
|
||||
=============================
|
||||
|
||||
The table garbage collection task can take a more elaborate
|
||||
configuration which is detailed below.
|
||||
|
||||
.. contents:: Table of contents
|
||||
|
||||
.. _table-garbage-collection-task-usage:
|
||||
|
||||
Using the garbage collection task
|
||||
=================================
|
||||
|
||||
The task can be registered to clean up a particular table, in which
|
||||
case you simply choose the table and the minimum age of the records to
|
||||
delete from the task configuration screen.
|
||||
|
||||
.. figure:: /Images/TableGarbageCollectionTaskConfiguration.png
|
||||
:alt: Table Garbage Collection task configuration
|
||||
|
||||
Configuring the table garbage collection task
|
||||
|
||||
In case no minimum age is choosen, the configured :php:`expirePeriod` is used.
|
||||
|
||||
.. figure:: /Images/TableGarbageCollectionTaskConfiguration-2.png
|
||||
:alt: Table Garbage Collection task configuration default expire period
|
||||
|
||||
Configuring the table garbage collection task with default expire period
|
||||
|
||||
It is also possible to clean up all configured table by
|
||||
checking the "Clean all available tables" box.
|
||||
|
||||
The configuration for
|
||||
the tables to clean up is stored in the TCA of table `tx_scheduler_task`, in
|
||||
field `tables`.
|
||||
|
||||
This configuration is an array with the table names as fields and the following
|
||||
entries:
|
||||
|
||||
- option :php:`expireField` can be used to point to a table field
|
||||
containing an expiry timestamp. This timestamp will then be used to
|
||||
decide whether a record has expired or not. If its timestamp is in the
|
||||
past, the record will be deleted.
|
||||
|
||||
- if a table has no expiry field, one can use a combination of a date
|
||||
field and an expiry period to decide which records should be deleted.
|
||||
The corresponding options are :php:`dateField` and :php:`expirePeriod`.
|
||||
The expiry period is expressed in days.
|
||||
|
||||
.. _table-garbage-collection-task-example:
|
||||
|
||||
Example: Configure additional tables for the "Garbage Collection" task
|
||||
======================================================================
|
||||
|
||||
.. deprecated:: 14.0
|
||||
The previous configuration method using
|
||||
:php:`$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][\TYPO3\CMS\Scheduler\Task\TableGarbageCollectionTask::class]['options']['tables']`
|
||||
has been deprecated and will be removed in TYPO3 v15.
|
||||
|
||||
See also: `Changelog Deprecation: #107550 - Table Garbage Collection Task configuration via $GLOBALS <https://docs.typo3.org/permalink/changelog:deprecation-107550-1736193200>`_
|
||||
|
||||
.. literalinclude:: _codesnippets/_tx_scheduler_garbage_collection.php.inc
|
||||
:language: php
|
||||
:caption: packages/my_extension/Configuration/TCA/Overrides/tx_scheduler_garbage_collection.php
|
||||
|
||||
.. include:: /_Includes/_ExtendingSchedulerTca.rst.txt
|
||||
|
||||
The first part of the configuration indicates that records older than
|
||||
180 days should be removed from table :code:`tx_myextension_my_table` ,
|
||||
based on the timestamp field called "tstamp". The second part
|
||||
indicates that old records should be removed from table
|
||||
:code:`tx_myextension_my_other_table` directly based on the field `expire`
|
||||
which contains expiration dates for each record.
|
||||
|
||||
|
||||
.. _table-garbage-collection-task-migration:
|
||||
|
||||
Migration: Supporting custom tables for garbage collection for both TYPO3 13 and 14
|
||||
===================================================================================
|
||||
|
||||
If your extension supports both TYPO3 13 (or below) and 14 keep the registration
|
||||
of additional tables in the extensions :file:`ext_localconf.php` until support
|
||||
for TYPO3 13 is removed:
|
||||
|
||||
.. literalinclude:: _codesnippets/_additional.php.inc
|
||||
:language: php
|
||||
:caption: packages/my_extension/ext_localconf.php
|
||||
@@ -0,0 +1,42 @@
|
||||
:navigation-title: Basic tasks
|
||||
|
||||
.. include:: /Includes.rst.txt
|
||||
.. _base-tasks:
|
||||
|
||||
==========================================
|
||||
The basic tasks provided by the TYPO3 Core
|
||||
==========================================
|
||||
|
||||
The Scheduler comes by default with several tasks:
|
||||
|
||||
- **Caching framework garbage collection** : some cache backends do not
|
||||
have an automatic garbage collection process. For these it is useful
|
||||
to run this Scheduler task to regularly free some space.
|
||||
|
||||
- **Fileadmin garbage collection** : empties :file:`_recycler_` folders in
|
||||
the fileadmin.
|
||||
|
||||
- **Table garbage collection** : cleans up old records from any table in
|
||||
the database. See related section below for more information on
|
||||
configuration.
|
||||
|
||||
Most TYPO3 console command can also be executed via scheduler.
|
||||
|
||||
The following tasks provide configuration options that need dedicated chapters:
|
||||
|
||||
.. toctree::
|
||||
:glob:
|
||||
:titlesonly:
|
||||
|
||||
*
|
||||
|
||||
.. _other-tasks:
|
||||
|
||||
Providing custom tasks from your extension
|
||||
==========================================
|
||||
|
||||
More tasks are provided by system extensions, such as the Extension
|
||||
Manager, which defines one for updating the available extensions list.
|
||||
|
||||
The base tasks are also there to serve as examples for task developers
|
||||
(see :ref:`developer-guide`).
|
||||
@@ -0,0 +1,53 @@
|
||||
:navigation-title: IP Anonymization task
|
||||
|
||||
.. include:: /Includes.rst.txt
|
||||
|
||||
.. _ip-anonymization-task:
|
||||
|
||||
=====================
|
||||
IP anonymization task
|
||||
=====================
|
||||
|
||||
The IP Anonymization task can take a more elaborate
|
||||
configuration which is detailed below.
|
||||
|
||||
The task anonymizes the IP addresses to enforce the privacy of the persisted data.
|
||||
|
||||
.. contents:: Table of contents
|
||||
|
||||
.. _ip-anonymization-task-example:
|
||||
|
||||
Example: Configure additional tables for the "IP Anonymization" task
|
||||
====================================================================
|
||||
|
||||
.. deprecated:: 14.0
|
||||
The previous configuration method using
|
||||
:php:`$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][\TYPO3\CMS\Scheduler\Task\IpAnonymizationTask::class]['options']['tables']`
|
||||
has been deprecated and will be removed in TYPO3 v15.
|
||||
|
||||
See also: `Changelog Deprecation: #107562 - Ip Anonymization Task configuration via $GLOBALS <https://docs.typo3.org/permalink/changelog:deprecation-107562-1736193200>`_
|
||||
|
||||
.. literalinclude:: _codesnippets/_tx_scheduler_ip_anonymization.php.inc
|
||||
:language: php
|
||||
:caption: packages/my_extension/Configuration/TCA/Overrides/tx_scheduler_ip_anonymization.php
|
||||
|
||||
.. include:: /_Includes/_ExtendingSchedulerTca.rst.txt
|
||||
|
||||
This entry configures that the field `private_ip` of table
|
||||
`tx_myextension_my_table` can be anonymized after a chosen number of days.
|
||||
|
||||
The field `tstamp` will be taken into account to determine when the database
|
||||
record was last changed.
|
||||
|
||||
.. _ip-anonymization-task-migration:
|
||||
|
||||
Migration: Supporting custom tables for "IP Anonymization" tasks for both TYPO3 13 and 14
|
||||
=========================================================================================
|
||||
|
||||
If your extension supports both TYPO3 13 (or below) and 14 keep the registration
|
||||
of additional tables in the extensions :file:`ext_localconf.php` until support
|
||||
for TYPO3 13 is removed:
|
||||
|
||||
.. literalinclude:: _codesnippets/_ext_localconf_ip_anonymization.php.inc
|
||||
:language: php
|
||||
:caption: packages/my_extension/ext_localconf.php
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
use TYPO3\CMS\Core\Information\Typo3Version;
|
||||
use TYPO3\CMS\Scheduler\Task\TableGarbageCollectionTask;
|
||||
|
||||
if ((new Typo3Version())->getMajorVersion() < 14) {
|
||||
// TODO: Remove once TYPO3 13 support is dropped
|
||||
// TYPO3 14 configuration can be found in Configuration/TCA/Overrides/tx_scheduler_garbage_collection.php
|
||||
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][TableGarbageCollectionTask::class]['options']['tables']['tx_myextension_errorlog'] = [
|
||||
'dateField' => 'tstamp',
|
||||
'expirePeriod' => '180',
|
||||
];
|
||||
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][TableGarbageCollectionTask::class]['options']['tables']['tx_myextension_uniqalias'] = [
|
||||
'expireField' => 'expire',
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
use TYPO3\CMS\Core\Information\Typo3Version;
|
||||
use TYPO3\CMS\Scheduler\Task\IpAnonymizationTask;
|
||||
|
||||
if ((new Typo3Version())->getMajorVersion() < 14) {
|
||||
// TODO: Remove once TYPO3 13 support is dropped
|
||||
// TYPO3 14 configuration can be found in Configuration/TCA/Overrides/tx_scheduler_ip_anonymization.php
|
||||
$garbageCollectionTables =& $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][IpAnonymizationTask::class]['options']['tables'];
|
||||
|
||||
$garbageCollectionTables = array_replace($garbageCollectionTables ?? [], [
|
||||
'tx_myextension_my_table' => [
|
||||
'dateField' => 'tstamp',
|
||||
'ipField' => 'private_ip',
|
||||
],
|
||||
]);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
use TYPO3\CMS\Scheduler\Task\TableGarbageCollectionTask;
|
||||
|
||||
if (isset($GLOBALS['TCA']['tx_scheduler_task'])) {
|
||||
$garbageCollectionTables =& $GLOBALS['TCA']['tx_scheduler_task']['types'][TableGarbageCollectionTask::class]['taskOptions']['tables'];
|
||||
|
||||
$garbageCollectionTables = array_replace($garbageCollectionTables ?? [], [
|
||||
'tx_myextension_my_table' => [
|
||||
'dateField' => 'tstamp',
|
||||
'expirePeriod' => 180,
|
||||
],
|
||||
'tx_myextension_my_other_table' => [
|
||||
'expireField' => 'expire',
|
||||
],
|
||||
]);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
use TYPO3\CMS\Scheduler\Task\IpAnonymizationTask;
|
||||
|
||||
if (isset($GLOBALS['TCA']['tx_scheduler_task'])) {
|
||||
$garbageCollectionTables =& $GLOBALS['TCA']['tx_scheduler_task']['types'][IpAnonymizationTask::class]['taskOptions']['tables'];
|
||||
|
||||
$garbageCollectionTables = array_replace($garbageCollectionTables ?? [], [
|
||||
'tx_myextension_my_table' => [
|
||||
'dateField' => 'tstamp',
|
||||
'ipField' => 'private_ip',
|
||||
],
|
||||
]);
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
:navigation-title: Task development
|
||||
|
||||
.. include:: /Includes.rst.txt
|
||||
.. _creating-tasks:
|
||||
|
||||
================================
|
||||
Creating a custom scheduler task
|
||||
================================
|
||||
|
||||
.. important::
|
||||
.. versionchanged:: 14.0
|
||||
Custom scheduler tasks can be registered as TCA types in table
|
||||
`tx_scheduler_task`.
|
||||
|
||||
See also: `Changelog Feature: #107526 - Custom TCA types for scheduler tasks <https://docs.typo3.org/permalink/changelog:feature-107526-1747816234>`_.
|
||||
|
||||
.. contents:: Table of contents
|
||||
|
||||
.. toctree::
|
||||
:glob:
|
||||
:titlesonly:
|
||||
|
||||
*
|
||||
|
||||
.. seealso::
|
||||
Symfony console commands can also be executed as scheduler task:
|
||||
See :ref:`Create and use Symfony commands in TYPO3 <t3coreapi:symfony-console-commands>`.
|
||||
|
||||
.. _creating-tasks-implementation:
|
||||
|
||||
Implementation of a custom scheduler task
|
||||
=========================================
|
||||
|
||||
All scheduler task implementations **must** extend
|
||||
:php:`\TYPO3\CMS\Scheduler\Task\AbstractTask`.
|
||||
|
||||
.. literalinclude:: _codesnippets/_MyTask.php.inc
|
||||
:language: php
|
||||
:caption: packages/my_extension/Classes/MyTask.php
|
||||
|
||||
A custom task implementation **must** override the method `execute(): bool`.
|
||||
It is the main method that is called when a task is executed.
|
||||
This method Should return `true` on successful execution, `false` on error.
|
||||
|
||||
.. note::
|
||||
There is no error handling by default, errors and failures are expected
|
||||
to be handled and logged by the client implementation.
|
||||
|
||||
Method `getAdditionalInformation()` **should** be implemented to provide
|
||||
additional information in the schedulers backend module.
|
||||
|
||||
Scheduler task implementations that provide `additional fields <https://docs.typo3.org/permalink/typo3/cms-scheduler:additional-fields>`_
|
||||
**should** implement additional methods, expecially `getTaskParameters()`.
|
||||
|
||||
.. _creating-tasks-registration:
|
||||
|
||||
Scheduler task registration and configuration
|
||||
=============================================
|
||||
|
||||
.. deprecated:: 14.0
|
||||
Registering tasks and additional field providers via
|
||||
:php:`$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks']` has
|
||||
been deprecated.
|
||||
|
||||
Custom scheduler tasks can be registered via TCA overrides, for example in
|
||||
:file:`EXT:my_extension/Configuration/TCA/Overrides/tx_scheduler_my_task.php`
|
||||
|
||||
.. literalinclude:: _codesnippets/_tx_scheduler_my_task.php.inc
|
||||
:language: php
|
||||
:caption: EXT:my_extension/Configuration/TCA/Overrides/tx_scheduler_my_task.php
|
||||
|
||||
.. tip::
|
||||
|
||||
Using the :php:`iconOverlay` option on task type registration, an icon
|
||||
overlay can be added, which is then displayed in the wizard. This can
|
||||
be useful for similar task types that use the same "base" `icon`, but
|
||||
still have to be differentiated.
|
||||
|
||||
.. include:: /_Includes/_ExtendingSchedulerTca.rst.txt
|
||||
|
||||
.. _additional-fields:
|
||||
|
||||
Providing additional fields for scheduler task
|
||||
==============================================
|
||||
|
||||
.. deprecated:: 14.0
|
||||
Registering tasks and additional field providers via
|
||||
:php:`$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks']` has
|
||||
been deprecated.
|
||||
|
||||
The :php-short:`\TYPO3\CMS\Scheduler\AdditionalFieldProviderInterface` and
|
||||
:php-short:`\TYPO3\CMS\Scheduler\AbstractAdditionalFieldProvider` have also
|
||||
been deprecated.
|
||||
|
||||
Tasks in general and additional fields for tasks are registered via TCA
|
||||
instead.
|
||||
|
||||
See also: `Migrating tasks with AdditionalFieldProviders to TCA registration <https://docs.typo3.org/permalink/typo3/cms-scheduler:additional-fields-migration>`_
|
||||
|
||||
Additional fields for scheduler tasks are handled via FormEngine and can be
|
||||
configured via TCA.
|
||||
|
||||
If the task should provide additional fields for configuration options in
|
||||
the backend module, you need to implement a second class, extending
|
||||
:php-short:`\TYPO3\CMS\Scheduler\AbstractAdditionalFieldProvider`.
|
||||
|
||||
The task needs to be registered via TCA override:
|
||||
|
||||
.. literalinclude:: _codesnippets/_scheduler_my_task_type-additional.php.inc
|
||||
:language: php
|
||||
:caption: EXT:my_extension/Configuration/TCA/Overrides/scheduler_my_task_type.php
|
||||
|
||||
And implemented the following methods in your scheduler task if needed:
|
||||
|
||||
.. literalinclude:: _codesnippets/_MyTaskWithAdditionalFields.php.inc
|
||||
:language: php
|
||||
:caption: packages/my_extension/Classes/MyTask.php
|
||||
|
||||
.. note::
|
||||
Method `getTaskParameters()` should be implemented when
|
||||
`migrating tasks <https://docs.typo3.org/permalink/typo3/cms-scheduler:additional-fields-migration>`_
|
||||
|
||||
For native TCA tasks, this method is typically no longer needed in custom
|
||||
tasks after the migration has been done, since field values are then stored
|
||||
directly in database columns.
|
||||
|
||||
.. seealso::
|
||||
There are additional examples in described in the
|
||||
`Changelog Feature: #107526 - Custom TCA types for scheduler tasks <https://docs.typo3.org/permalink/changelog:feature-107526-1747816234>`_.
|
||||
@@ -0,0 +1,110 @@
|
||||
:navigation-title: Migration
|
||||
|
||||
.. include:: /Includes.rst.txt
|
||||
|
||||
.. _task-migration:
|
||||
|
||||
=====================================================
|
||||
Migration to the TCA registration for scheduler tasks
|
||||
=====================================================
|
||||
|
||||
.. deprecated:: 14.0
|
||||
Registering tasks and additional field providers via
|
||||
:php:`$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks']` has
|
||||
been deprecated.
|
||||
|
||||
The :php-short:`\TYPO3\CMS\Scheduler\AdditionalFieldProviderInterface` and
|
||||
:php-short:`\TYPO3\CMS\Scheduler\AbstractAdditionalFieldProvider` have also
|
||||
been deprecated.
|
||||
|
||||
.. contents:: Table of contents
|
||||
|
||||
Tasks in general and additional fields for tasks are registered via TCA
|
||||
instead.
|
||||
.. _additional-fields-migration:
|
||||
|
||||
Migrating tasks with AdditionalFieldProviders to TCA registration
|
||||
=================================================================
|
||||
|
||||
Scheduler tasks should now be registered as native task types using TCA.
|
||||
This provides a more integrated and maintainable approach to task configuration.
|
||||
|
||||
.. _additional-fields-migration-steps:
|
||||
|
||||
Migration steps:
|
||||
----------------
|
||||
|
||||
1. Remove the registration from :file:`ext_localconf.php`
|
||||
2. Create a TCA override file in :file:`Configuration/TCA/Overrides/scheduler_my_task_type.php`
|
||||
3. Update your task class to implement the new parameter methods
|
||||
4. Remove the :php:`AdditionalFieldProvider` class if it exists
|
||||
|
||||
.. note::
|
||||
The new TCA-based approach automatically migrates existing task data.
|
||||
When upgrading, existing task configurations are preserved through the
|
||||
:php:`getTaskParameters()` and :php:`setTaskParameters()` methods.
|
||||
|
||||
.. _additional-fields-migration-example:
|
||||
|
||||
Example migration: Scheduler task with additional fields suppporting TYPO3 13 and 14
|
||||
------------------------------------------------------------------------------------
|
||||
|
||||
Remove the registration from :file:`ext_localconf.php` once TYPO3 13 support is
|
||||
dropped:
|
||||
|
||||
.. literalinclude:: _codesnippets/_ext_localconf_deprecated.php.inc
|
||||
:language: php
|
||||
:caption: packages/my_extension/ext_localconf.php
|
||||
|
||||
And also remove the :php:`MyTaskAdditionalFieldProvider` class once
|
||||
TYPO3 13 support is dropped.
|
||||
|
||||
Create a TCA override file in :file:`Configuration/TCA/Overrides/scheduler_my_task_type.php`:
|
||||
|
||||
.. literalinclude:: _codesnippets/_scheduler_my_task_type-additional.php.inc
|
||||
:language: php
|
||||
:caption: EXT:my_extension/Configuration/TCA/Overrides/scheduler_my_task_type.php
|
||||
|
||||
Update your (existing) task class to implement the new methods:
|
||||
|
||||
.. literalinclude:: _codesnippets/_MyTaskWithAdditionalFieldsMigration.php.inc
|
||||
:language: php
|
||||
:caption: packages/my_extension/Classes/MyTask.php
|
||||
|
||||
The new TCA-based approach uses three key methods for parameter handling:
|
||||
|
||||
**getTaskParameters(): array**
|
||||
This method is already implemented in ``AbstractTask`` to handle task class
|
||||
properties automatically, but can be overridden in task classes for custom
|
||||
behavior.
|
||||
|
||||
The method is primarily used:
|
||||
|
||||
* For migration from old serialized task format to new TCA structure
|
||||
* For non-native (deprecated) task types to store their values in the legacy ``parameters`` field
|
||||
|
||||
For native TCA tasks, this method is typically no longer needed in custom
|
||||
tasks after the migration has been done, since field values are then stored
|
||||
directly in database columns.
|
||||
|
||||
**setTaskParameters(array $parameters): void**
|
||||
Sets field values from an associative array. This method handles:
|
||||
|
||||
* Migration from old AdditionalFieldProvider field names to new TCA field names
|
||||
* Loading saved task configurations when editing or executing tasks
|
||||
* Parameter mapping during task creation and updates
|
||||
* The method should always be implemented, especially for native tasks
|
||||
|
||||
The migration pattern is: :php:`$this->myField = $parameters['oldName'] ?? $parameters['new_tca_field_name'] ?? '';`
|
||||
|
||||
**validateTaskParameters(array $parameters): bool**
|
||||
*Optional method.* Only implement this for validation that cannot be handled by FormEngine.
|
||||
|
||||
* Basic validation (required, trim, etc.) should be done via TCA configuration (``required`` property and ``eval`` options)
|
||||
* Use this method for complex business logic validation (e.g., email format validation, external API checks)
|
||||
* Return ``false`` and add FlashMessage for validation errors
|
||||
* FormEngine automatically handles standard TCA validation rules
|
||||
|
||||
For a complete working example, see :php:`\TYPO3\CMS\Reports\Task\SystemStatusUpdateTask`
|
||||
and its corresponding TCA configuration in
|
||||
:file:`EXT:reports/Configuration/TCA/Overrides/scheduler_system_status_update_task.php`.
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MyVendor\MyExtension\Task;
|
||||
|
||||
use MyVendor\MyExtension\BusinessLogic;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Scheduler\Task\AbstractTask;
|
||||
|
||||
final class MyTask extends AbstractTask
|
||||
{
|
||||
/**
|
||||
* MUST be implemented by all tasks
|
||||
*/
|
||||
public function execute(): bool
|
||||
{
|
||||
# Dependency injection cannot be used in scheduler tasks
|
||||
$businessLogic = GeneralUtility::makeInstance(BusinessLogic::class);
|
||||
return $businessLogic->run('arg1', 'arg2', '…');
|
||||
}
|
||||
|
||||
public function getAdditionalInformation()
|
||||
{
|
||||
$this->getLanguageService()->sL('LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:myTaskInformation');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MyVendor\MyExtension\Task;
|
||||
|
||||
use MyVendor\MyExtension\BusinessLogic;
|
||||
use TYPO3\CMS\Core\Messaging\FlashMessage;
|
||||
use TYPO3\CMS\Core\Messaging\FlashMessageService;
|
||||
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Scheduler\Task\AbstractTask;
|
||||
|
||||
final class MyTask extends AbstractTask
|
||||
{
|
||||
protected string $myField = '';
|
||||
protected string $emailList = '';
|
||||
|
||||
public function execute(): bool
|
||||
{
|
||||
# Dependency injection cannot be used in scheduler tasks
|
||||
$businessLogic = GeneralUtility::makeInstance(BusinessLogic::class);
|
||||
return $businessLogic->run($this->myField, $this->emailList, '…');
|
||||
}
|
||||
|
||||
/**
|
||||
* Set field values from associative array.
|
||||
*
|
||||
* @param array $parameters Values from TCA fields
|
||||
*/
|
||||
public function setTaskParameters(array $parameters): void
|
||||
{
|
||||
$this->myField = $parameters['my_extension_field'] ?? '';
|
||||
$this->emailList = $parameters['my_extension_email_list'] ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate task parameters.
|
||||
* Only implement this method for validation that cannot be handled by FormEngine.
|
||||
* Basic validation like 'required' should be done via TCA 'eval' configuration.
|
||||
*/
|
||||
public function validateTaskParameters(array $parameters): bool
|
||||
{
|
||||
$isValid = true;
|
||||
|
||||
// Example: Custom email validation (beyond basic 'required' check)
|
||||
$emailList = $parameters['my_extension_email_list'] ?? '';
|
||||
if (!empty($emailList)) {
|
||||
$emails = GeneralUtility::trimExplode(',', $emailList, true);
|
||||
foreach ($emails as $email) {
|
||||
if (!GeneralUtility::validEmail($email)) {
|
||||
GeneralUtility::makeInstance(FlashMessageService::class)
|
||||
->getMessageQueueByIdentifier()
|
||||
->addMessage(
|
||||
GeneralUtility::makeInstance(
|
||||
FlashMessage::class,
|
||||
'Invalid email address: ' . $email,
|
||||
'',
|
||||
ContextualFeedbackSeverity::ERROR
|
||||
)
|
||||
);
|
||||
$isValid = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $isValid;
|
||||
}
|
||||
public function getAdditionalInformation(): string
|
||||
{
|
||||
return sprintf(
|
||||
'Field: %s, Emails: %s',
|
||||
$this->myField,
|
||||
$this->emailList
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MyVendor\MyExtension\Task;
|
||||
|
||||
use MyVendor\MyExtension\BusinessLogic;
|
||||
use TYPO3\CMS\Core\Messaging\FlashMessage;
|
||||
use TYPO3\CMS\Core\Messaging\FlashMessageService;
|
||||
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Scheduler\Task\AbstractTask;
|
||||
|
||||
final class MyTask extends AbstractTask
|
||||
{
|
||||
protected string $myField = '';
|
||||
protected string $emailList = '';
|
||||
|
||||
|
||||
public function execute(): bool
|
||||
{
|
||||
# Dependency injection cannot be used in scheduler tasks
|
||||
$businessLogic = GeneralUtility::makeInstance(BusinessLogic::class);
|
||||
return $businessLogic->run($this->myField, $this->emailList, '…');
|
||||
}
|
||||
|
||||
/**
|
||||
* Return current field values as associative array.
|
||||
* This method is called during migration from old serialized tasks
|
||||
* and when displaying task information.
|
||||
*/
|
||||
public function getTaskParameters(): array
|
||||
{
|
||||
return [
|
||||
'my_extension_field' => $this->myField,
|
||||
'my_extension_email_list' => $this->emailList,
|
||||
];
|
||||
}
|
||||
/**
|
||||
* Set field values from associative array.
|
||||
* This method handles both old and new parameter formats for migration.
|
||||
*
|
||||
* @param array $parameters Values from either old AdditionalFieldProvider or new TCA fields
|
||||
*/
|
||||
public function setTaskParameters(array $parameters): void
|
||||
{
|
||||
// Handle migration: check old parameter names first, then new TCA field names
|
||||
$this->myField = $parameters['myField'] ?? $parameters['my_extension_field'] ?? '';
|
||||
$this->emailList = $parameters['emailList'] ?? $parameters['my_extension_email_list'] ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate task parameters.
|
||||
* Only implement this method for validation that cannot be handled by FormEngine.
|
||||
* Basic validation like 'required' should be done via TCA 'eval' configuration.
|
||||
*/
|
||||
public function validateTaskParameters(array $parameters): bool
|
||||
{
|
||||
$isValid = true;
|
||||
|
||||
// Example: Custom email validation (beyond basic 'required' check)
|
||||
$emailList = $parameters['my_extension_email_list'] ?? '';
|
||||
if (!empty($emailList)) {
|
||||
$emails = GeneralUtility::trimExplode(',', $emailList, true);
|
||||
foreach ($emails as $email) {
|
||||
if (!GeneralUtility::validEmail($email)) {
|
||||
GeneralUtility::makeInstance(FlashMessageService::class)
|
||||
->getMessageQueueByIdentifier()
|
||||
->addMessage(
|
||||
GeneralUtility::makeInstance(
|
||||
FlashMessage::class,
|
||||
'Invalid email address: ' . $email,
|
||||
'',
|
||||
ContextualFeedbackSeverity::ERROR
|
||||
)
|
||||
);
|
||||
$isValid = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $isValid;
|
||||
}
|
||||
public function getAdditionalInformation(): string
|
||||
{
|
||||
$info = [];
|
||||
if ($this->myField !== '') {
|
||||
$info[] = 'Field: ' . $this->myField;
|
||||
}
|
||||
if ($this->emailList !== '') {
|
||||
$info[] = 'Emails: ' . $this->emailList;
|
||||
}
|
||||
return implode(', ', $info);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use MyVendor\MyExtension\Task\MyTask;
|
||||
|
||||
if ((new \TYPO3\CMS\Core\Information\Typo3Version())->getMajorVersion() < 14) {
|
||||
// Todo: Remove when TYPO3 13 support is dropped
|
||||
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][MyTask::class] = [
|
||||
'extension' => 'my_extension',
|
||||
'title' => 'LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:myTask.title',
|
||||
'description' => 'LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:myTask.description',
|
||||
'additionalFields' => \MyVendor\MyExtension\Task\MyTaskAdditionalFieldProvider::class,
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
|
||||
use MyVendor\MyExtension\Task\MyTask;
|
||||
|
||||
defined('TYPO3') or die();
|
||||
|
||||
if (isset($GLOBALS['TCA']['tx_scheduler_task'])) {
|
||||
// Add custom fields to the tx_scheduler_task table
|
||||
ExtensionManagementUtility::addTCAcolumns(
|
||||
'tx_scheduler_task',
|
||||
[
|
||||
'my_extension_field' => [
|
||||
'label' => 'LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:field.label',
|
||||
'config' => [
|
||||
'type' => 'input',
|
||||
'size' => 30,
|
||||
'required' => true,
|
||||
'eval' => 'trim',
|
||||
'placeholder' => 'Enter value here...',
|
||||
],
|
||||
],
|
||||
'my_extension_email_list' => [
|
||||
'label' => 'LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:emailList.label',
|
||||
'config' => [
|
||||
'type' => 'text',
|
||||
'rows' => 3,
|
||||
'required' => true,
|
||||
'placeholder' => 'admin@example.com',
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
// Register the task type
|
||||
ExtensionManagementUtility::addRecordType(
|
||||
[
|
||||
'label' => 'Some title or LLL:EXT reference',
|
||||
'description' => 'Some description or LLL:EXT reference',
|
||||
'value' => MyTask::class,
|
||||
'icon' => 'mimetypes-x-tx_scheduler_task_group',
|
||||
'iconOverlay' => 'content-clock',
|
||||
'group' => 'my_extension',
|
||||
],
|
||||
'
|
||||
--div--;core.form.tabs:general,
|
||||
tasktype,
|
||||
task_group,
|
||||
description,
|
||||
my_extension_field,
|
||||
my_extension_email_list,
|
||||
--div--;core.form.tabs:timing,
|
||||
execution_details,
|
||||
nextexecution,
|
||||
--palette--;;lastexecution,
|
||||
--div--;core.form.tabs:access,
|
||||
disable,
|
||||
--div--;core.form.tabs:extended,',
|
||||
[],
|
||||
'',
|
||||
'tx_scheduler_task'
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use MyVendor\MyExtension\Task\MyTask;
|
||||
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
|
||||
|
||||
defined('TYPO3') or die();
|
||||
|
||||
if (isset($GLOBALS['TCA']['tx_scheduler_task'])) {
|
||||
ExtensionManagementUtility::addRecordType(
|
||||
[
|
||||
'label' => 'My Custom Task',
|
||||
'description' => 'Description of what this task does',
|
||||
'value' => MyTask::class,
|
||||
'icon' => 'my-custom-icon',
|
||||
'iconOverlay' => 'content-clock',
|
||||
'group' => 'my_extension',
|
||||
],
|
||||
$GLOBALS['TCA']['tx_scheduler_task']['types']['0']['showitem'],
|
||||
[],
|
||||
'',
|
||||
'tx_scheduler_task'
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
:navigation-title: Events
|
||||
|
||||
.. include:: /Includes.rst.txt
|
||||
.. _scheduler-events:
|
||||
|
||||
==========================================
|
||||
Events provided by the scheduler extension
|
||||
==========================================
|
||||
|
||||
The system extension :composer:`typo3/cms-scheduler` provides the following
|
||||
events:
|
||||
|
||||
.. contents:: Table of contents
|
||||
|
||||
.. _ModifyNewSchedulerTaskWizardItemsEvent:
|
||||
|
||||
ModifyNewSchedulerTaskWizardItemsEvent
|
||||
======================================
|
||||
|
||||
The PSR-14 event :php:`\TYPO3\CMS\Scheduler\Event\ModifyNewSchedulerTaskWizardItemsEvent`
|
||||
allows extensions to modify the items in the
|
||||
`scheduler task wizard <https://docs.typo3.org/permalink/typo3/cms-scheduler:adding-editing-task-wizard>`_.
|
||||
|
||||
.. seealso::
|
||||
|
||||
`ModifyNewSchedulerTaskWizardItemsEvent (TYPO3 explained) <https://docs.typo3.org/permalink/t3coreapi:ModifyNewSchedulerTaskWizardItemsEvent>`_
|
||||
for examples and the api information.
|
||||
|
||||
.. _AfterTaskExecutionEvent:
|
||||
|
||||
AfterTaskExecutionEvent
|
||||
=======================
|
||||
|
||||
The PSR-14 event :php:`\TYPO3\CMS\Scheduler\Event\AfterTaskExecutionEvent`
|
||||
is dispatched after a scheduled task (including command tasks) has been executed.
|
||||
It provides the following information:
|
||||
|
||||
* :php:`getTask(): AbstractTask` — the executed task object
|
||||
* :php:`isSuccess(): bool` — whether the task completed without exception
|
||||
* :php:`getException(): ?\Throwable` — the thrown exception on failure, or :php:`null` on success
|
||||
|
||||
Example listener::
|
||||
|
||||
use TYPO3\CMS\Scheduler\Event\AfterTaskExecutionEvent;
|
||||
|
||||
final class SchedulerTaskResultListener
|
||||
{
|
||||
public function __invoke(AfterTaskExecutionEvent $event): void
|
||||
{
|
||||
$task = $event->getTask();
|
||||
$status = $event->isSuccess() ? 'success' : 'failure';
|
||||
// e.g. send a notification, write to a custom log, etc.
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
:navigation-title: Development
|
||||
|
||||
.. include:: /Includes.rst.txt
|
||||
.. _developer-guide:
|
||||
|
||||
==========================
|
||||
Scheduler task development
|
||||
==========================
|
||||
|
||||
The Scheduler makes it very easy to create a new task class.
|
||||
Furthermore the tasks packaged with this extension provide
|
||||
a good basis to learn by example.
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 5
|
||||
:titlesonly:
|
||||
:glob:
|
||||
|
||||
*/Index
|
||||
@@ -0,0 +1,30 @@
|
||||
.. include:: /Includes.rst.txt
|
||||
.. _scheduler-api:
|
||||
|
||||
=============
|
||||
Scheduler API
|
||||
=============
|
||||
|
||||
It is possible to refer to the Scheduler from other extensions. Once a
|
||||
:php:`\TYPO3\CMS\Scheduler\Scheduler` object has been instantiated all of its
|
||||
public methods can be used. The PHPdoc of the methods should be enough to
|
||||
understand what each is to be used for.
|
||||
|
||||
The extension ships with a
|
||||
:php:`\TYPO3\CMS\Scheduler\Domain\Repository\SchedulerTaskRepository` class,
|
||||
which provides some helpful methods, for example:
|
||||
|
||||
* :php:`findByUid(int $uid)`: this method is used to fetch a registered task
|
||||
from the database given an ID.
|
||||
|
||||
* :php:`findNextExecutableTask()`: this method returns the next due task. The
|
||||
return value is the unserialized task object.
|
||||
|
||||
* :php:`findRecordByUid(int $uid)`: is also used to retrieve a registered task
|
||||
from the database, but it returns the record corresponding to the task
|
||||
registration and not the task object itself.
|
||||
|
||||
These are the main methods that will be used from outside the
|
||||
Scheduler as they can retrieve registered tasks from the database.
|
||||
When a task has been fetched, all public methods from the
|
||||
:php:`\TYPO3\CMS\Scheduler\Task\AbstractTask` class can be used.
|
||||
@@ -0,0 +1,58 @@
|
||||
.. include:: /Includes.rst.txt
|
||||
.. _technical-background:
|
||||
.. _scheduler-task-storage:
|
||||
|
||||
======================
|
||||
Scheduler task storage
|
||||
======================
|
||||
|
||||
.. versionchanged:: 14.0
|
||||
With TYPO3 v14.0 the storage of scheduler tasks switched from the
|
||||
PHP-serialized storage format in the database to a JSON-based format.
|
||||
|
||||
The scheduler tasks displayed in backend module :guilabel:`Administration > Scheduler`
|
||||
are stored in the **database** table :sql:`tx_scheduler_task`. Task groups are
|
||||
stored in table :sql:`tx_scheduler_task_group`.
|
||||
|
||||
.. contents:: Table of contents
|
||||
|
||||
.. _scheduler-task-storage-fields:
|
||||
|
||||
tx_scheduler_task fields
|
||||
========================
|
||||
|
||||
The database table :sql:`tx_scheduler_task` contains the following fields:
|
||||
|
||||
`tasktype`
|
||||
Typically contains the fully qualified class name of the task, for example
|
||||
:php:`\TYPO3\CMS\Scheduler\Task\CachingFrameworkGarbageCollectionTask` or
|
||||
the command name, for example `language:update` if the task is implemented as
|
||||
`Symfony console command <https://docs.typo3.org/permalink/t3coreapi:symfony-console-commands>`_.
|
||||
`parameters`
|
||||
The options configuring the task as JSON-encoded value.
|
||||
`execution_details`
|
||||
Contains all details on execution like the type (Recurring / Single),
|
||||
start and end dates and the frequency in which recurring tasks should be
|
||||
executed.
|
||||
|
||||
Additionally it stores some information on the last and next execution of the
|
||||
task, and fields for a description and the group.
|
||||
|
||||
.. _serialized-objects:
|
||||
.. _save-task-state:
|
||||
.. _serialized-objects_migration:
|
||||
|
||||
Migration from serialized task objects
|
||||
======================================
|
||||
|
||||
.. attention::
|
||||
|
||||
.. versionchanged:: 14.0
|
||||
|
||||
The storage format was changed with TYPO3 v14. If the database field
|
||||
:sql:`tx_scheduler_task:tasktype` is empty this hints at a missing or failed
|
||||
migration by the upgrade wizard. See also:
|
||||
`Important: #106532 - Changed database storage format for Scheduler Tasks <https://docs.typo3.org/permalink/changelog:important-106532-1744207039>`_
|
||||
|
||||
A manual migration is necessary. You can also delete the task and
|
||||
create it newly.
|
||||
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 75 KiB |
|
After Width: | Height: | Size: 12 KiB |