Files
cms-scheduler/Classes/Service/TaskService.php
T

275 lines
12 KiB
PHP

<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Scheduler\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;
}
}