TYPO3 v15 dev-main snapshot ()

This commit is contained in:
2026-08-10 22:31:38 +02:00
commit 4392dbe2ce
142 changed files with 11824 additions and 0 deletions
+326
View File
@@ -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);
}
}
+73
View File
@@ -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;
}
}
+193
View File
@@ -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;
}
}
+193
View File
@@ -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);
}
}
+188
View File
@@ -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;
}
}
+110
View File
@@ -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;
}
}
+43
View File
@@ -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 = [],
) {}
}