TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -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 = [],
|
||||
) {}
|
||||
}
|
||||
Reference in New Issue
Block a user