first commit

This commit is contained in:
salam committed 2026-10-19 08:18:39 +02:00
commit 1218dd1517
12716 files changed
+989233

No files matched your search

@@ -0,0 +1,40 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Scheduler;
/*
* 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!
*/
use TYPO3\CMS\Core\Messaging\FlashMessage;
use TYPO3\CMS\Core\Messaging\FlashMessageService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Base class for providers of additional fields
*/
abstract class AbstractAdditionalFieldProvider implements AdditionalFieldProviderInterface
{
/**
* Add a flash message
*
* @param string $message the flash message content
* @param int $severity the flash message severity
*/
protected function addMessage(string $message, int $severity = FlashMessage::OK): void
{
$flashMessage = GeneralUtility::makeInstance(FlashMessage::class, $message, '', $severity);
$service = GeneralUtility::makeInstance(FlashMessageService::class);
$queue = $service->getMessageQueueByIdentifier();
$queue->enqueue($flashMessage);
}
}
@@ -0,0 +1,48 @@
<?php
namespace TYPO3\CMS\Scheduler;
/*
* 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!
*/
/**
* Interface for classes who want to provide additional fields when adding a task
*/
interface AdditionalFieldProviderInterface
{
/**
* Gets additional fields to render in the form to add/edit a task
*
* @param array $taskInfo Values of the fields from the add/edit task form
* @param \TYPO3\CMS\Scheduler\Task\AbstractTask $task The task object being edited. Null when adding a task!
* @param \TYPO3\CMS\Scheduler\Controller\SchedulerModuleController $schedulerModule Reference to the scheduler backend module
* @return array A two dimensional array, array('Identifier' => array('fieldId' => array('code' => '', 'label' => '', 'cshKey' => '', 'cshLabel' => ''))
*/
public function getAdditionalFields(array &$taskInfo, $task, \TYPO3\CMS\Scheduler\Controller\SchedulerModuleController $schedulerModule);
/**
* Validates the additional fields' values
*
* @param array $submittedData An array containing the data submitted by the add/edit task form
* @param \TYPO3\CMS\Scheduler\Controller\SchedulerModuleController $schedulerModule Reference to the scheduler backend module
* @return bool TRUE if validation was ok (or selected class is not relevant), FALSE otherwise
*/
public function validateAdditionalFields(array &$submittedData, \TYPO3\CMS\Scheduler\Controller\SchedulerModuleController $schedulerModule);
/**
* Takes care of saving the additional fields' values in the task's object
*
* @param array $submittedData An array containing the data submitted by the add/edit task form
* @param \TYPO3\CMS\Scheduler\Task\AbstractTask $task Reference to the scheduler backend module
*/
public function saveAdditionalFields(array $submittedData, \TYPO3\CMS\Scheduler\Task\AbstractTask $task);
}
@@ -0,0 +1,161 @@
<?php
namespace TYPO3\CMS\Scheduler\Command;
/*
* 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!
*/
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use TYPO3\CMS\Core\Core\Bootstrap;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Scheduler\Scheduler;
use TYPO3\CMS\Scheduler\Task\AbstractTask;
/**
* CLI command for the 'scheduler' extension which executes
*/
class SchedulerCommand extends Command
{
/**
* @var bool
*/
protected $hasTask = true;
/**
* @var Scheduler
*/
protected $scheduler;
/**
* Configure the command by defining the name, options and arguments
*/
public function configure()
{
$this
->setDescription('Start the TYPO3 Scheduler from the command line.')
->setHelp('If no parameter is given, the scheduler executes any tasks that are overdue to run.
Call it like this: typo3/sysext/core/bin/typo3 scheduler:run --task=13 -f')
->addOption(
'task',
'i',
InputOption::VALUE_REQUIRED,
'UID of a specific task'
)
->addOption(
'force',
'f',
InputOption::VALUE_NONE,
'Force execution of the task which is passed with -i option'
)
->addOption(
'stop',
's',
InputOption::VALUE_NONE,
'Stop the task which is passed with -i option'
);
}
/**
* Execute scheduler tasks
*
* @param InputInterface $input
* @param OutputInterface $output
*/
public function execute(InputInterface $input, OutputInterface $output)
{
// Make sure the _cli_ user is loaded
Bootstrap::initializeBackendAuthentication();
$this->scheduler = GeneralUtility::makeInstance(Scheduler::class);
if ((int)$input->getOption('task') > 0) {
$taskUid = (int)$input->getOption('task');
$stopTask = (bool)($input->hasOption('stop') && $input->getOption('stop'));
$force = (bool)($input->hasOption('force') && $input->getOption('force'));
$task = $this->getTask($taskUid, $stopTask || $force);
if ($this->scheduler->isValidTaskObject($task)) {
if ($stopTask) {
$this->stopTask($task);
} else {
$this->scheduler->executeTask($task);
}
}
// Record the run in the system registry
$this->scheduler->recordLastRun('cli-by-id');
} else {
$this->loopTasks();
}
}
/**
* Stop task
*
* @param AbstractTask $task
*/
protected function stopTask($task)
{
if ($this->scheduler->isValidTaskObject($task)) {
$task->unmarkAllExecutions();
}
}
/**
* Return task a task for a given UID
*
* @param int $taskUid
* @param bool $force fetch the task regardless if it is queued for execution
* @return AbstractTask
*/
protected function getTask(int $taskUid, bool $force)
{
if ($force) {
$task = $this->scheduler->fetchTask($taskUid);
} else {
$whereClause = 'uid = ' . (int)$taskUid . ' AND nextexecution != 0 AND nextexecution <= ' . $GLOBALS['EXEC_TIME'];
list($task) = $this->scheduler->fetchTasksWithCondition($whereClause);
}
return $task;
}
/**
* Execute tasks in loop that are ready to execute
*/
protected function loopTasks()
{
do {
// Try getting the next task and execute it
// If there are no more tasks to execute, an exception is thrown by \TYPO3\CMS\Scheduler\Scheduler::fetchTask()
try {
$task = $this->scheduler->fetchTask();
try {
$this->scheduler->executeTask($task);
} catch (\Exception $e) {
// We ignore any exception that may have been thrown during execution,
// as this is a background process.
// The exception message has been recorded to the database anyway
continue;
}
} catch (\OutOfBoundsException $e) {
$this->hasTask = false;
} catch (\UnexpectedValueException $e) {
continue;
}
} while ($this->hasTask);
// Record the run in the system registry
$this->scheduler->recordLastRun();
}
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,225 @@
<?php
namespace TYPO3\CMS\Scheduler\CronCommand;
/*
* 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!
*/
/**
* This class provides calculations for the cron command format.
*/
class CronCommand
{
/**
* Normalized sections of the cron command.
* Allowed are comma separated lists of integers and the character '*'
*
* field lower and upper bound
* ----- --------------
* minute 0-59
* hour 0-23
* day of month 1-31
* month 1-12
* day of week 1-7
*
* @var array $cronCommandSections
*/
protected $cronCommandSections;
/**
* Timestamp of next execution date.
* This value starts with 'now + 1 minute' if not set externally
* by unit tests. After a call to calculateNextValue() it holds the timestamp of
* the next execution date which matches the cron command restrictions.
*/
protected $timestamp;
/**
* Constructor
*
* @param string $cronCommand The cron command can hold any combination documented as valid
* @param bool|int $timestamp Optional start time, used in unit tests
*/
public function __construct($cronCommand, $timestamp = false)
{
$cronCommand = NormalizeCommand::normalize($cronCommand);
// Explode cron command to sections
$this->cronCommandSections = \TYPO3\CMS\Core\Utility\GeneralUtility::trimExplode(' ', $cronCommand);
// Initialize the values with the starting time
// This takes care that the calculated time is always in the future
if ($timestamp === false) {
$timestamp = strtotime('+1 minute');
} else {
$timestamp += 60;
}
$this->timestamp = $this->roundTimestamp($timestamp);
}
/**
* Calculates the date of the next execution.
*
* @throws \RuntimeException
*/
public function calculateNextValue()
{
$newTimestamp = $this->getTimestamp();
// Calculate next minute and hour field
$loopCount = 0;
while (true) {
$loopCount++;
// If there was no match within two days, cron command is invalid.
// The second day is needed to catch the summertime leap in some countries.
if ($loopCount > 2880) {
throw new \RuntimeException('Unable to determine next execution timestamp: Hour and minute combination is invalid.', 1291494126);
}
if ($this->minuteAndHourMatchesCronCommand($newTimestamp)) {
break;
}
$newTimestamp += 60;
}
$loopCount = 0;
while (true) {
$loopCount++;
// A date must match within the next 4 years, this high number makes
// sure leap year cron command configuration are caught.
// If the loop runs longer than that, the cron command is invalid.
if ($loopCount > 1464) {
throw new \RuntimeException('Unable to determine next execution timestamp: Day of month, month and day of week combination is invalid.', 1291501280);
}
if ($this->dayMatchesCronCommand($newTimestamp)) {
break;
}
$newTimestamp += $this->numberOfSecondsInDay($newTimestamp);
}
$this->timestamp = $newTimestamp;
}
/**
* Get next timestamp
*
* @return int Unix timestamp
*/
public function getTimestamp()
{
return $this->timestamp;
}
/**
* Get cron command sections. Array of strings, each containing either
* a list of comma separated integers or *
*
* @return array command sections:
* @internal
*/
public function getCronCommandSections()
{
return $this->cronCommandSections;
}
/**
* Determine if current timestamp matches minute and hour cron command restriction.
*
* @param int $timestamp to test
* @return bool TRUE if cron command conditions are met
*/
protected function minuteAndHourMatchesCronCommand($timestamp)
{
$minute = (int)date('i', $timestamp);
$hour = (int)date('G', $timestamp);
$commandMatch = false;
if ($this->isInCommandList($this->cronCommandSections[0], $minute) && $this->isInCommandList($this->cronCommandSections[1], $hour)) {
$commandMatch = true;
}
return $commandMatch;
}
/**
* Determine if current timestamp matches day of month, month and day of week
* cron command restriction
*
* @param int $timestamp to test
* @return bool TRUE if cron command conditions are met
*/
protected function dayMatchesCronCommand($timestamp)
{
$dayOfMonth = date('j', $timestamp);
$month = date('n', $timestamp);
$dayOfWeek = date('N', $timestamp);
$isInDayOfMonth = $this->isInCommandList($this->cronCommandSections[2], $dayOfMonth);
$isInMonth = $this->isInCommandList($this->cronCommandSections[3], $month);
$isInDayOfWeek = $this->isInCommandList($this->cronCommandSections[4], $dayOfWeek);
// Quote from vixiecron:
// Note: The day of a command's execution can be specified by two fields — day of month, and day of week.
// If both fields are restricted (i.e., aren't *), the command will be run when either field
// matches the current time. For example, `30 4 1,15 * 5' would cause
// a command to be run at 4:30 am on the 1st and 15th of each month, plus every Friday.
$isDayOfMonthRestricted = (string)$this->cronCommandSections[2] !== '*';
$isDayOfWeekRestricted = (string)$this->cronCommandSections[4] !== '*';
$commandMatch = false;
if ($isInMonth) {
if ($isInDayOfMonth && $isDayOfMonthRestricted || $isInDayOfWeek && $isDayOfWeekRestricted || $isInDayOfMonth && !$isDayOfMonthRestricted && $isInDayOfWeek && !$isDayOfWeekRestricted) {
$commandMatch = true;
}
}
return $commandMatch;
}
/**
* Determine if a given number validates a cron command section. The given cron
* command must be a 'normalized' list with only comma separated integers or '*'
*
* @param string $commandExpression: cron command
* @param int $numberToMatch: number to look up
* @return bool TRUE if number is in list
*/
protected function isInCommandList($commandExpression, $numberToMatch)
{
if ((string)$commandExpression === '*') {
$inList = true;
} else {
$inList = \TYPO3\CMS\Core\Utility\GeneralUtility::inList($commandExpression, $numberToMatch);
}
return $inList;
}
/**
* Helper method to calculate number of seconds in a day.
*
* This is not always 86400 (60*60*24) and depends on the timezone:
* Some countries like Germany have a summertime / wintertime switch,
* on every last sunday in march clocks are forwarded by one hour (set from 2:00 to 3:00),
* and on last sunday of october they are set back one hour (from 3:00 to 2:00).
* This shortens and lengthens the length of a day by one hour.
*
* @param int $timestamp Unix timestamp
* @return int Number of seconds of day
*/
protected function numberOfSecondsInDay($timestamp)
{
$now = mktime(0, 0, 0, date('n', $timestamp), date('j', $timestamp), date('Y', $timestamp));
// Make sure to be in next day, even if day has 25 hours
$nextDay = $now + 60 * 60 * 25;
$nextDay = mktime(0, 0, 0, date('n', $nextDay), date('j', $nextDay), date('Y', $nextDay));
return $nextDay - $now;
}
/**
* Round a timestamp down to full minute.
*
* @param int $timestamp Unix timestamp
* @return int Rounded timestamp
*/
protected function roundTimestamp($timestamp)
{
return mktime(date('H', $timestamp), date('i', $timestamp), 0, date('n', $timestamp), date('j', $timestamp), date('Y', $timestamp));
}
}
@@ -0,0 +1,364 @@
<?php
namespace TYPO3\CMS\Scheduler\CronCommand;
/*
* 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!
*/
use TYPO3\CMS\Core\Utility\MathUtility;
/**
* Validate and normalize a cron command.
*
* Special fields like three letter weekdays, ranges and steps are substituted
* to a comma separated list of integers. Example:
* '2-4 10-40/10 * mar * fri' will be normalized to '2,4 10,20,30,40 * * 3 1,2'
*/
class NormalizeCommand
{
/**
* Main API method: Get the cron command and normalize it.
*
* If no exception is thrown, the resulting cron command is validated
* and consists of five whitespace separated fields, which are either
* the letter '*' or a sorted, unique comma separated list of integers.
*
* @throws \InvalidArgumentException cron command is invalid or out of bounds
* @param string $cronCommand The cron command to normalize
* @return string Normalized cron command
*/
public static function normalize($cronCommand)
{
$cronCommand = trim($cronCommand);
$cronCommand = self::convertKeywordsToCronCommand($cronCommand);
$cronCommand = self::normalizeFields($cronCommand);
return $cronCommand;
}
/**
* Accept special cron command keywords and convert to standard cron syntax.
* Allowed keywords: @yearly, @annually, @monthly, @weekly, @daily, @midnight, @hourly
*
* @param string $cronCommand Cron command
* @return string Normalized cron command if keyword was found, else unchanged cron command
*/
protected static function convertKeywordsToCronCommand($cronCommand)
{
switch ($cronCommand) {
case '@yearly':
case '@annually':
$cronCommand = '0 0 1 1 *';
break;
case '@monthly':
$cronCommand = '0 0 1 * *';
break;
case '@weekly':
$cronCommand = '0 0 * * 0';
break;
case '@daily':
case '@midnight':
$cronCommand = '0 0 * * *';
break;
case '@hourly':
$cronCommand = '0 * * * *';
break;
}
return $cronCommand;
}
/**
* Normalize cron command field to list of integers or *
*
* @param string $cronCommand cron command
* @return string Normalized cron command
*/
protected static function normalizeFields($cronCommand)
{
$fieldArray = self::splitFields($cronCommand);
$fieldArray[0] = self::normalizeIntegerField($fieldArray[0], 0, 59);
$fieldArray[1] = self::normalizeIntegerField($fieldArray[1], 0, 23);
$fieldArray[2] = self::normalizeIntegerField($fieldArray[2], 1, 31);
$fieldArray[3] = self::normalizeMonthAndWeekdayField($fieldArray[3], true);
$fieldArray[4] = self::normalizeMonthAndWeekdayField($fieldArray[4], false);
$normalizedCronCommand = implode(' ', $fieldArray);
return $normalizedCronCommand;
}
/**
* Split a given cron command like '23 * * * *' to an array with five fields.
*
* @throws \InvalidArgumentException If splitted array does not contain five entries
* @param string $cronCommand cron command
* @return array
*/
protected static function splitFields($cronCommand)
{
$fields = explode(' ', $cronCommand);
if (count($fields) !== 5) {
throw new \InvalidArgumentException('Unable to split given cron command to five fields.', 1291227373);
}
return $fields;
}
/**
* Normalize month field.
*
* @param string $expression Month field expression
* @param bool $isMonthField TRUE if month field is handled, FALSE for weekday field
* @return string Normalized expression
*/
protected static function normalizeMonthAndWeekdayField($expression, $isMonthField = true)
{
if ((string)$expression === '*') {
$fieldValues = '*';
} else {
// Fragment expression by , / and - and substitute three letter code of month and weekday to numbers
$listOfCommaValues = explode(',', $expression);
$fieldArray = [];
foreach ($listOfCommaValues as $listElement) {
if (strpos($listElement, '/') !== false) {
list($left, $right) = explode('/', $listElement);
if (strpos($left, '-') !== false) {
list($leftBound, $rightBound) = explode('-', $left);
$leftBound = self::normalizeMonthAndWeekday($leftBound, $isMonthField);
$rightBound = self::normalizeMonthAndWeekday($rightBound, $isMonthField);
$left = $leftBound . '-' . $rightBound;
} else {
if ((string)$left !== '*') {
$left = self::normalizeMonthAndWeekday($left, $isMonthField);
}
}
$fieldArray[] = $left . '/' . $right;
} elseif (strpos($listElement, '-') !== false) {
list($left, $right) = explode('-', $listElement);
$left = self::normalizeMonthAndWeekday($left, $isMonthField);
$right = self::normalizeMonthAndWeekday($right, $isMonthField);
$fieldArray[] = $left . '-' . $right;
} else {
$fieldArray[] = self::normalizeMonthAndWeekday($listElement, $isMonthField);
}
}
$fieldValues = implode(',', $fieldArray);
}
return $isMonthField ? self::normalizeIntegerField($fieldValues, 1, 12) : self::normalizeIntegerField($fieldValues, 1, 7);
}
/**
* Normalize integer field.
*
* @throws \InvalidArgumentException If field is invalid or out of bounds
* @param string $expression Expression
* @param int $lowerBound Lower limit of result list
* @param int $upperBound Upper limit of result list
* @return string Normalized expression
*/
protected static function normalizeIntegerField($expression, $lowerBound = 0, $upperBound = 59)
{
if ((string)$expression === '*') {
$fieldValues = '*';
} else {
$listOfCommaValues = explode(',', $expression);
$fieldArray = [];
foreach ($listOfCommaValues as $listElement) {
if (strpos($listElement, '/') !== false) {
list($left, $right) = explode('/', $listElement);
if ((string)$left === '*') {
$leftList = self::convertRangeToListOfValues($lowerBound . '-' . $upperBound);
} else {
$leftList = self::convertRangeToListOfValues($left);
}
$fieldArray[] = self::reduceListOfValuesByStepValue($leftList . '/' . $right);
} elseif (strpos($listElement, '-') !== false) {
$fieldArray[] = self::convertRangeToListOfValues($listElement);
} elseif (MathUtility::canBeInterpretedAsInteger($listElement)) {
$fieldArray[] = $listElement;
} elseif (strlen($listElement) === 2 && $listElement[0] === '0') {
$fieldArray[] = (int)$listElement;
} else {
throw new \InvalidArgumentException('Unable to normalize integer field.', 1291429389);
}
}
$fieldValues = implode(',', $fieldArray);
}
if ((string)$fieldValues === '') {
throw new \InvalidArgumentException('Unable to convert integer field to list of values: Result list empty.', 1291422012);
}
if ((string)$fieldValues !== '*') {
$fieldList = explode(',', $fieldValues);
sort($fieldList);
$fieldList = array_unique($fieldList);
if (current($fieldList) < $lowerBound) {
throw new \InvalidArgumentException('Lowest element in list is smaller than allowed.', 1291470084);
}
if (end($fieldList) > $upperBound) {
throw new \InvalidArgumentException('An element in the list is higher than allowed.', 1291470170);
}
$fieldValues = implode(',', $fieldList);
}
return (string)$fieldValues;
}
/**
* Convert a range of integers to a list: 4-6 results in a string '4,5,6'
*
* @throws \InvalidArgumentException If range can not be converted to list
* @param string $range Integer-integer
* @return string
*/
protected static function convertRangeToListOfValues($range)
{
if ((string)$range === '') {
throw new \InvalidArgumentException('Unable to convert range to list of values with empty string.', 1291234985);
}
$rangeArray = explode('-', $range);
// Sanitize fields and cast to integer
foreach ($rangeArray as $fieldNumber => $fieldValue) {
if (!MathUtility::canBeInterpretedAsInteger($fieldValue)) {
throw new \InvalidArgumentException('Unable to convert value to integer.', 1291237668);
}
$rangeArray[$fieldNumber] = (int)$fieldValue;
}
$rangeArrayCount = count($rangeArray);
if ($rangeArrayCount === 1) {
$resultList = $rangeArray[0];
} elseif ($rangeArrayCount === 2) {
$left = $rangeArray[0];
$right = $rangeArray[1];
if ($left > $right) {
throw new \InvalidArgumentException('Unable to convert range to list: Left integer must not be greater than right integer.', 1291237145);
}
$resultListArray = [];
for ($i = $left; $i <= $right; $i++) {
$resultListArray[] = $i;
}
$resultList = implode(',', $resultListArray);
} else {
throw new \InvalidArgumentException('Unable to convert range to list of values.', 1291234986);
}
return (string)$resultList;
}
/**
* Reduce a given list of values by step value.
* Following a range with ``/<number>'' specifies skips of the number's value through the range.
* 1-5/2 -> 1,3,5
* 2-10/3 -> 2,5,8
*
* @throws \InvalidArgumentException if step value is invalid or if resulting list is empty
* @param string $stepExpression Step value expression
* @return string Comma separated list of valid values
*/
protected static function reduceListOfValuesByStepValue($stepExpression)
{
if ($stepExpression === '') {
throw new \InvalidArgumentException('Unable to convert step values.', 1291234987);
}
$stepValuesAndStepArray = explode('/', $stepExpression);
$stepValuesAndStepArrayCount = count($stepValuesAndStepArray);
if ($stepValuesAndStepArrayCount < 1 || $stepValuesAndStepArrayCount > 2) {
throw new \InvalidArgumentException('Unable to convert step values: Multiple slashes found.', 1291242168);
}
$left = $stepValuesAndStepArray[0] ?? '';
$right = $stepValuesAndStepArray[1] ?? '';
if ($left === '') {
throw new \InvalidArgumentException('Unable to convert step values: Left part of / is empty.', 1291414955);
}
if ($right === '') {
throw new \InvalidArgumentException('Unable to convert step values: Right part of / is empty.', 1291414956);
}
if (!MathUtility::canBeInterpretedAsInteger($right)) {
throw new \InvalidArgumentException('Unable to convert step values: Right part must be a single integer.', 1291414957);
}
$right = (int)$right;
$leftArray = explode(',', $left);
$validValues = [];
$currentStep = $right;
foreach ($leftArray as $leftValue) {
if (!MathUtility::canBeInterpretedAsInteger($leftValue)) {
throw new \InvalidArgumentException('Unable to convert step values: Left part must be a single integer or comma separated list of integers.', 1291414958);
}
if ($currentStep === 0) {
$currentStep = $right;
}
if ($currentStep === $right) {
$validValues[] = (int)$leftValue;
}
$currentStep--;
}
if (empty($validValues)) {
throw new \InvalidArgumentException('Unable to convert step values: Result value list is empty.', 1291414959);
}
return implode(',', $validValues);
}
/**
* Dispatcher method for normalizeMonth and normalizeWeekday
*
* @param string $expression Month or weekday to be normalized
* @param bool $isMonth TRUE if a month is handled, FALSE for weekday
* @return string normalized month or weekday
*/
protected static function normalizeMonthAndWeekday($expression, $isMonth = true)
{
$expression = $isMonth ? self::normalizeMonth($expression) : self::normalizeWeekday($expression);
return (string)$expression;
}
/**
* Accept a string representation or integer number of a month like
* 'jan', 'February', 01, ... and convert to normalized integer value 1 .. 12
*
* @throws \InvalidArgumentException If month string can not be converted to integer
* @param string $month Month representation
* @return int month integer representation between 1 and 12
*/
protected static function normalizeMonth($month)
{
$timestamp = strtotime('2010-' . $month . '-01');
// timestamp must be >= 2010-01-01 and <= 2010-12-01
if (!$timestamp || $timestamp < strtotime('2010-01-01') || $timestamp > strtotime('2010-12-01')) {
throw new \InvalidArgumentException('Unable to convert given month name.', 1291083486);
}
return (int)date('n', $timestamp);
}
/**
* Accept a string representation or integer number of a weekday like
* 'mon', 'Friday', 3, ... and convert to normalized integer value 1 .. 7
*
* @throws \InvalidArgumentException If weekday string can not be converted
* @param string $weekday Weekday representation
* @return int weekday integer representation between 1 and 7
*/
protected static function normalizeWeekday($weekday)
{
$normalizedWeekday = false;
// 0 (sunday) -> 7
if ((string)$weekday === '0') {
$weekday = 7;
}
if ($weekday >= 1 && $weekday <= 7) {
$normalizedWeekday = (int)$weekday;
}
if (!$normalizedWeekday) {
// Convert string representation like 'sun' to integer
$timestamp = strtotime('next ' . $weekday, mktime(0, 0, 0, 1, 1, 2010));
if (!$timestamp || $timestamp < strtotime('2010-01-01') || $timestamp > strtotime('2010-01-08')) {
throw new \InvalidArgumentException('Unable to convert given weekday name.', 1291163589);
}
$normalizedWeekday = (int)date('N', $timestamp);
}
return $normalizedWeekday;
}
}
@@ -0,0 +1,56 @@
<?php
namespace TYPO3\CMS\Scheduler\Example;
/*
* 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!
*/
/**
* Provides a task that sleeps for some time
* This is useful for testing parallel executions
* @internal This class is an example is not considered part of the Public TYPO3 API.
*/
class SleepTask extends \TYPO3\CMS\Scheduler\Task\AbstractTask
{
/**
* Number of seconds the task should be sleeping for
*
* @var int
*/
public $sleepTime = 10;
/**
* Function executed from the Scheduler.
* Goes to sleep ;-)
*
* @return bool
*/
public function execute()
{
$time = 10;
if (!empty($this->sleepTime)) {
$time = $this->sleepTime;
}
sleep($time);
return true;
}
/**
* This method returns the sleep duration as additional information
*
* @return string Information to display
*/
public function getAdditionalInformation()
{
return $GLOBALS['LANG']->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:label.sleepTime') . ': ' . $this->sleepTime;
}
}
@@ -0,0 +1,99 @@
<?php
namespace TYPO3\CMS\Scheduler\Example;
/*
* 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!
*/
use TYPO3\CMS\Core\Messaging\FlashMessage;
use TYPO3\CMS\Scheduler\AbstractAdditionalFieldProvider;
use TYPO3\CMS\Scheduler\Controller\SchedulerModuleController;
use TYPO3\CMS\Scheduler\Task\AbstractTask;
use TYPO3\CMS\Scheduler\Task\Enumeration\Action;
/**
* Additional fields provider class for usage with the Scheduler's sleep task
* @internal This class is an example is not considered part of the Public TYPO3 API.
*/
class SleepTaskAdditionalFieldProvider extends AbstractAdditionalFieldProvider
{
/**
* This method is used to define new fields for adding or editing a task
* In this case, it adds an sleep time field
*
* @param array $taskInfo Reference to the array containing the info used in the add/edit form
* @param AbstractTask|null $task When editing, reference to the current task. NULL when adding.
* @param SchedulerModuleController $schedulerModule Reference to the calling object (Scheduler's BE module)
* @return array Array containing all the information pertaining to the additional fields
*/
public function getAdditionalFields(array &$taskInfo, $task, SchedulerModuleController $schedulerModule)
{
$currentSchedulerModuleAction = $schedulerModule->getCurrentAction();
// Initialize extra field value
if (empty($taskInfo['sleepTime'])) {
if ($currentSchedulerModuleAction->equals(Action::ADD)) {
// In case of new task and if field is empty, set default sleep time
$taskInfo['sleepTime'] = 30;
} elseif ($currentSchedulerModuleAction->equals(Action::EDIT)) {
// In case of edit, set to internal value if no data was submitted already
$taskInfo['sleepTime'] = $task->sleepTime;
} else {
// Otherwise set an empty value, as it will not be used anyway
$taskInfo['sleepTime'] = '';
}
}
// Write the code for the field
$fieldID = 'task_sleepTime';
$fieldCode = '<input type="text" class="form-control" name="tx_scheduler[sleepTime]" id="' . $fieldID . '" value="' . $taskInfo['sleepTime'] . '" size="10">';
$additionalFields = [];
$additionalFields[$fieldID] = [
'code' => $fieldCode,
'label' => 'LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:label.sleepTime',
'cshKey' => '_MOD_system_txschedulerM1',
'cshLabel' => $fieldID
];
return $additionalFields;
}
/**
* This method checks any additional data that is relevant to the specific task
* If the task class is not relevant, the method is expected to return TRUE
*
* @param array $submittedData Reference to the array containing the data submitted by the user
* @param SchedulerModuleController $schedulerModule Reference to the calling object (Scheduler's BE module)
* @return bool TRUE if validation was ok (or selected class is not relevant), FALSE otherwise
*/
public function validateAdditionalFields(array &$submittedData, SchedulerModuleController $schedulerModule)
{
$submittedData['sleepTime'] = (int)$submittedData['sleepTime'];
if ($submittedData['sleepTime'] < 0) {
$this->addMessage($GLOBALS['LANG']->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:msg.invalidSleepTime'), FlashMessage::ERROR);
$result = false;
} else {
$result = true;
}
return $result;
}
/**
* This method is used to save any additional input into the current task object
* if the task class matches
*
* @param array $submittedData Array containing the data submitted by the user
* @param AbstractTask $task Reference to the current task object
*/
public function saveAdditionalFields(array $submittedData, AbstractTask $task)
{
$task->sleepTime = $submittedData['sleepTime'];
}
}
@@ -0,0 +1,91 @@
<?php
namespace TYPO3\CMS\Scheduler\Example;
/*
* 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!
*/
use TYPO3\CMS\Core\Core\Environment;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Provides testing procedures
* @internal This class is an example is not considered part of the Public TYPO3 API.
*/
class TestTask extends \TYPO3\CMS\Scheduler\Task\AbstractTask
{
/**
* An email address to be used during the process
*
* @var string $email
*/
public $email;
/**
* Function executed from the Scheduler.
* Sends an email
*
* @return bool
*/
public function execute()
{
$success = false;
if (!empty($this->email)) {
// If an email address is defined, send a message to it
$this->logger->info('[TYPO3\\CMS\\Scheduler\\Example\\TestTask]: Test email sent to "' . $this->email . '"');
// Get execution information
$exec = $this->getExecution();
// Get call method
if (Environment::isCli()) {
$calledBy = 'CLI module dispatcher';
$site = '-';
} else {
$calledBy = 'TYPO3 backend';
$site = GeneralUtility::getIndpEnv('TYPO3_SITE_URL');
}
$start = $exec->getStart();
$end = $exec->getEnd();
$interval = $exec->getInterval();
$multiple = $exec->getMultiple();
$cronCmd = $exec->getCronCmd();
$mailBody = 'SCHEDULER TEST-TASK' . LF . '- - - - - - - - - - - - - - - -' . LF . 'UID: ' . $this->taskUid . LF . 'Sitename: ' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'] . LF . 'Site: ' . $site . LF . 'Called by: ' . $calledBy . LF . 'tstamp: ' . date('Y-m-d H:i:s') . ' [' . time() . ']' . LF . 'maxLifetime: ' . $this->scheduler->extConf['maxLifetime'] . LF . 'start: ' . date('Y-m-d H:i:s', $start) . ' [' . $start . ']' . LF . 'end: ' . (empty($end) ? '-' : date('Y-m-d H:i:s', $end) . ' [' . $end . ']') . LF . 'interval: ' . $interval . LF . 'multiple: ' . ($multiple ? 'yes' : 'no') . LF . 'cronCmd: ' . ($cronCmd ? $cronCmd : 'not used');
// Prepare mailer and send the mail
try {
/** @var \TYPO3\CMS\Core\Mail\MailMessage $mailer */
$mailer = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Mail\MailMessage::class);
$mailer->setFrom([$this->email => 'SCHEDULER TEST-TASK']);
$mailer->setReplyTo([$this->email => 'SCHEDULER TEST-TASK']);
$mailer->setSubject('SCHEDULER TEST-TASK');
$mailer->setBody($mailBody);
$mailer->setTo($this->email);
$mailsSend = $mailer->send();
$success = $mailsSend > 0;
} catch (\Exception $e) {
throw new \TYPO3\CMS\Core\Exception($e->getMessage(), 1476048416);
}
} else {
// No email defined, just log the task
$this->logger->warning('[TYPO3\\CMS\\Scheduler\\Example\\TestTask]: No email address given');
}
return $success;
}
/**
* This method returns the destination mail address as additional information
*
* @return string Information to display
*/
public function getAdditionalInformation()
{
return $GLOBALS['LANG']->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:label.email') . ': ' . $this->email;
}
}
@@ -0,0 +1,102 @@
<?php
namespace TYPO3\CMS\Scheduler\Example;
/*
* 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!
*/
use TYPO3\CMS\Core\Messaging\FlashMessage;
use TYPO3\CMS\Scheduler\AbstractAdditionalFieldProvider;
use TYPO3\CMS\Scheduler\Controller\SchedulerModuleController;
use TYPO3\CMS\Scheduler\Task\AbstractTask;
use TYPO3\CMS\Scheduler\Task\Enumeration\Action;
/**
* Additional fields provider class for usage with the Scheduler's test task
* @internal This class is an example is not considered part of the Public TYPO3 API.
*/
class TestTaskAdditionalFieldProvider extends AbstractAdditionalFieldProvider
{
/**
* This method is used to define new fields for adding or editing a task
* In this case, it adds an email field
*
* @param array $taskInfo Reference to the array containing the info used in the add/edit form
* @param AbstractTask|null $task When editing, reference to the current task. NULL when adding.
* @param SchedulerModuleController $schedulerModule Reference to the calling object (Scheduler's BE module)
* @return array Array containing all the information pertaining to the additional fields
*/
public function getAdditionalFields(array &$taskInfo, $task, SchedulerModuleController $schedulerModule)
{
$currentSchedulerModuleAction = $schedulerModule->getCurrentAction();
// Initialize extra field value
if (empty($taskInfo['email'])) {
if ($currentSchedulerModuleAction->equals(Action::ADD)) {
// In case of new task and if field is empty, set default email address
$taskInfo['email'] = $GLOBALS['BE_USER']->user['email'];
} elseif ($currentSchedulerModuleAction->equals(Action::EDIT)) {
// In case of edit, and editing a test task, set to internal value if not data was submitted already
$taskInfo['email'] = $task->email;
} else {
// Otherwise set an empty value, as it will not be used anyway
$taskInfo['email'] = '';
}
}
// Write the code for the field
$fieldID = 'task_email';
$fieldCode = '<input type="text" class="form-control" name="tx_scheduler[email]" id="' . $fieldID . '" value="' . htmlspecialchars($taskInfo['email']) . '" size="30">';
$additionalFields = [];
$additionalFields[$fieldID] = [
'code' => $fieldCode,
'label' => 'LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:label.email',
'cshKey' => '_MOD_system_txschedulerM1',
'cshLabel' => $fieldID
];
return $additionalFields;
}
/**
* This method checks any additional data that is relevant to the specific task
* If the task class is not relevant, the method is expected to return TRUE
*
* @param array $submittedData Reference to the array containing the data submitted by the user
* @param SchedulerModuleController $schedulerModule Reference to the calling object (Scheduler's BE module)
* @return bool TRUE if validation was ok (or selected class is not relevant), FALSE otherwise
*/
public function validateAdditionalFields(array &$submittedData, SchedulerModuleController $schedulerModule)
{
$submittedData['email'] = trim($submittedData['email']);
if (empty($submittedData['email'])) {
$this->addMessage(
$GLOBALS['LANG']->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:msg.noEmail'),
FlashMessage::ERROR
);
$result = false;
} else {
$result = true;
}
return $result;
}
/**
* This method is used to save any additional input into the current task object
* if the task class matches
*
* @param array $submittedData Array containing the data submitted by the user
* @param \TYPO3\CMS\Scheduler\Task\AbstractTask $task Reference to the current task object
*/
public function saveAdditionalFields(array $submittedData, AbstractTask $task)
{
$task->email = $submittedData['email'];
}
}
@@ -0,0 +1,281 @@
<?php
namespace TYPO3\CMS\Scheduler;
/*
* 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!
*/
/**
* This class manages the logic of a particular execution of a task
* @internal
*/
class Execution
{
/**
* Start date of a task (timestamp)
*
* @var int
*/
protected $start;
/**
* End date of a task (timestamp)
*
* @var int
*/
protected $end;
/**
* Interval between executions (in seconds)
*
* @var int
*/
protected $interval;
/**
* Flag for concurrent executions: TRUE if allowed, FALSE otherwise (default)
*
* @var bool
*/
protected $multiple = false;
/**
* The cron command string of this task,
*
* @var string
*/
protected $cronCmd;
/**
* This flag is used to mark a new single execution
* See explanations in method setIsNewSingleExecution()
*
* @var bool
* @see \TYPO3\CMS\Scheduler\Execution::setIsNewSingleExecution()
*/
protected $isNewSingleExecution = false;
/**********************************
* Setters and getters
**********************************/
/**
* This method is used to set the start date
*
* @param int $start Start date (timestamp)
*/
public function setStart($start)
{
$this->start = $start;
}
/**
* This method is used to get the start date
*
* @return int Start date (timestamp)
*/
public function getStart()
{
return $this->start;
}
/**
* This method is used to set the end date
*
* @param int $end End date (timestamp)
*/
public function setEnd($end)
{
$this->end = $end;
}
/**
* This method is used to get the end date
*
* @return int End date (timestamp)
*/
public function getEnd()
{
return $this->end;
}
/**
* This method is used to set the interval
*
* @param int $interval Interval (in seconds)
*/
public function setInterval($interval)
{
$this->interval = $interval;
}
/**
* This method is used to get the interval
*
* @return int Interval (in seconds)
*/
public function getInterval()
{
return $this->interval;
}
/**
* This method is used to set the multiple execution flag
*
* @param bool $multiple TRUE if concurrent executions are allowed, FALSE otherwise
*/
public function setMultiple($multiple)
{
$this->multiple = $multiple;
}
/**
* This method is used to get the multiple execution flag
*
* @return bool TRUE if concurrent executions are allowed, FALSE otherwise
*/
public function getMultiple()
{
return $this->multiple;
}
/**
* Set the value of the cron command
*
* @param string $cmd Cron command, using cron-like syntax
*/
public function setCronCmd($cmd)
{
$this->cronCmd = $cmd;
}
/**
* Get the value of the cron command
*
* @return string Cron command, using cron-like syntax
*/
public function getCronCmd()
{
return $this->cronCmd;
}
/**
* Set whether this is a newly created single execution.
* This is necessary for the following reason: if a new single-running task
* is created and its start date is in the past (even for only a few seconds),
* the next run time calculation (which happens upon saving) will disable
* that task, because it was meant to run only once and is in the past.
* Setting this flag to TRUE preserves this task for a single run.
* Upon next execution, this flag is set to FALSE.
*
* @param bool $isNewSingleExecution Is newly created single execution?
* @see \TYPO3\CMS\Scheduler\Execution::getNextExecution()
*/
public function setIsNewSingleExecution($isNewSingleExecution)
{
$this->isNewSingleExecution = $isNewSingleExecution;
}
/**
* Get whether this is a newly created single execution
*
* @return bool Is newly created single execution?
*/
public function getIsNewSingleExecution()
{
return $this->isNewSingleExecution;
}
/**********************************
* Execution calculations and logic
**********************************/
/**
* This method gets or calculates the next execution date
*
* @return int Timestamp of the next execution
* @throws \OutOfBoundsException
*/
public function getNextExecution()
{
if ($this->getIsNewSingleExecution()) {
$this->setIsNewSingleExecution(false);
return $this->start;
}
if (!$this->isEnded()) {
// If the schedule has not yet run out, find out the next date
if (!$this->isStarted()) {
// If the schedule hasn't started yet, next date is start date
$date = $this->start;
} else {
// If the schedule has already started, calculate next date
if ($this->cronCmd) {
// If it uses cron-like syntax, calculate next date
$date = $this->getNextCronExecution();
} elseif ($this->interval == 0) {
// If not and there's no interval either, it's a singe execution: use start date
$date = $this->start;
} else {
// Otherwise calculate date based on interval
$now = time();
$date = $now + $this->interval - ($now - $this->start) % $this->interval;
}
// If date is in the future, throw an exception
if (!empty($this->end) && $date > $this->end) {
throw new \OutOfBoundsException('Next execution date is past end date.', 1250715528);
}
}
} else {
// The event has ended, throw an exception
throw new \OutOfBoundsException('Task is past end date.', 1250715544);
}
return $date;
}
/**
* Calculates the next execution from a cron command
*
* @return int Next execution (timestamp)
*/
public function getNextCronExecution()
{
/** @var \TYPO3\CMS\Scheduler\CronCommand\CronCommand $cronCmd */
$cronCmd = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Scheduler\CronCommand\CronCommand::class, $this->getCronCmd());
$cronCmd->calculateNextValue();
return $cronCmd->getTimestamp();
}
/**
* Checks if the schedule for a task is started or not
*
* @return bool TRUE if the schedule is already active, FALSE otherwise
*/
public function isStarted()
{
return $this->start < time();
}
/**
* Checks if the schedule for a task is passed or not
*
* @return bool TRUE if the schedule is not active anymore, FALSE otherwise
*/
public function isEnded()
{
if (empty($this->end)) {
// If no end is defined, the schedule never ends
$result = false;
} else {
// Otherwise check if end is in the past
$result = $this->end < time();
}
return $result;
}
}
@@ -0,0 +1,22 @@
<?php
namespace TYPO3\CMS\Scheduler;
/*
* 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!
*/
/**
* Failed execution exception
*/
class FailedExecutionException extends \RuntimeException
{
}
@@ -0,0 +1,28 @@
<?php
namespace TYPO3\CMS\Scheduler;
/*
* 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!
*/
/**
* Interface for tasks who can provide their progress
*/
interface ProgressProviderInterface
{
/**
* Gets the progress of a task.
*
* @return float Progress of the task as a two decimal precision float. f.e. 44.87
*/
public function getProgress();
}
@@ -0,0 +1,490 @@
<?php
namespace TYPO3\CMS\Scheduler;
/*
* 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!
*/
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use Psr\Log\LoggerInterface;
use TYPO3\CMS\Core\Configuration\ExtensionConfiguration;
use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\QueryHelper;
use TYPO3\CMS\Core\Log\LogManager;
use TYPO3\CMS\Core\Registry;
use TYPO3\CMS\Core\SingletonInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* TYPO3 Scheduler. This class handles scheduling and execution of tasks.
*/
class Scheduler implements SingletonInterface, LoggerAwareInterface
{
use LoggerAwareTrait;
/**
* @var array $extConf Settings from the extension manager
*/
public $extConf = [];
/**
* Constructor, makes sure all derived client classes are included
*/
public function __construct()
{
// Get configuration from the extension manager
$this->extConf = GeneralUtility::makeInstance(ExtensionConfiguration::class)->get('scheduler');
if (empty($this->extConf['maxLifetime'])) {
$this->extConf['maxLifetime'] = 1440;
}
// Clean up the serialized execution arrays
$this->cleanExecutionArrays();
}
/**
* Adds a task to the pool
*
* @param Task\AbstractTask $task The object representing the task to add
* @return bool TRUE if the task was successfully added, FALSE otherwise
*/
public function addTask(Task\AbstractTask $task)
{
$taskUid = $task->getTaskUid();
if (empty($taskUid)) {
$fields = [
'crdate' => $GLOBALS['EXEC_TIME'],
'disable' => (int)$task->isDisabled(),
'description' => $task->getDescription(),
'task_group' => $task->getTaskGroup(),
'serialized_task_object' => 'RESERVED'
];
$connection = GeneralUtility::makeInstance(ConnectionPool::class)
->getConnectionForTable('tx_scheduler_task');
$result = $connection->insert(
'tx_scheduler_task',
$fields,
['serialized_task_object' => Connection::PARAM_LOB]
);
if ($result) {
$task->setTaskUid($connection->lastInsertId('tx_scheduler_task'));
$task->save();
$result = true;
} else {
$result = false;
}
} else {
$result = false;
}
return $result;
}
/**
* Cleans the execution lists of the scheduled tasks, executions older than 24h are removed
* @todo find a way to actually kill the job
*/
protected function cleanExecutionArrays()
{
$tstamp = $GLOBALS['EXEC_TIME'];
$connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
$queryBuilder = $connectionPool->getQueryBuilderForTable('tx_scheduler_task');
// Select all tasks with executions
// NOTE: this cleanup is done for disabled tasks too,
// to avoid leaving old executions lying around
$result = $queryBuilder->select('uid', 'serialized_executions', 'serialized_task_object')
->from('tx_scheduler_task')
->where(
$queryBuilder->expr()->neq(
'serialized_executions',
$queryBuilder->createNamedParameter('', \PDO::PARAM_STR)
),
$queryBuilder->expr()->eq('deleted', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT))
)
->execute();
$maxDuration = $this->extConf['maxLifetime'] * 60;
while ($row = $result->fetch()) {
$executions = [];
if ($serialized_executions = unserialize($row['serialized_executions'])) {
foreach ($serialized_executions as $task) {
if ($tstamp - $task < $maxDuration) {
$executions[] = $task;
} else {
$task = unserialize($row['serialized_task_object']);
$this->log('Removing logged execution, assuming that the process is dead. Execution of \'' . get_class($task) . '\' (UID: ' . $row['uid'] . ') was started at ' . date('Y-m-d H:i:s', $task->getExecutionTime()));
}
}
}
$executionCount = count($executions);
if (count($serialized_executions) !== $executionCount) {
if ($executionCount === 0) {
$value = '';
} else {
$value = serialize($executions);
}
$connectionPool->getConnectionForTable('tx_scheduler_task')->update(
'tx_scheduler_task',
['serialized_executions' => $value],
['uid' => (int)$row['uid']],
['serialized_executions' => Connection::PARAM_LOB]
);
}
}
}
/**
* This method executes the given task and properly marks and records that execution
* It is expected to return FALSE if the task was barred from running or if it was not saved properly
*
* @param Task\AbstractTask $task The task to execute
* @return bool Whether the task was saved successfully to the database or not
* @throws \Throwable
*/
public function executeTask(Task\AbstractTask $task)
{
$task->setRunOnNextCronJob(false);
// Trigger the saving of the task, as this will calculate its next execution time
// This should be calculated all the time, even if the execution is skipped
// (in case it is skipped, this pushes back execution to the next possible date)
$task->save();
// Set a scheduler object for the task again,
// as it was removed during the save operation
$task->setScheduler();
$result = true;
// Task is already running and multiple executions are not allowed
if (!$task->areMultipleExecutionsAllowed() && $task->isExecutionRunning()) {
// Log multiple execution error
$logMessage = 'Task is already running and multiple executions are not allowed, skipping! Class: ' . get_class($task) . ', UID: ' . $task->getTaskUid();
$this->logger->info($logMessage);
$result = false;
} else {
// Log scheduler invocation
$this->logger->info('Start execution. Class: ' . get_class($task) . ', UID: ' . $task->getTaskUid());
// Register execution
$executionID = $task->markExecution();
$failure = null;
try {
// Execute task
$successfullyExecuted = $task->execute();
if (!$successfullyExecuted) {
throw new FailedExecutionException('Task failed to execute successfully. Class: ' . get_class($task) . ', UID: ' . $task->getTaskUid(), 1250596541);
}
} catch (\Throwable $e) {
// Store exception, so that it can be saved to database
$failure = $e;
}
// Un-register execution
$task->unmarkExecution($executionID, $failure);
// Log completion of execution
$this->logger->info('Task executed. Class: ' . get_class($task) . ', UID: ' . $task->getTaskUid());
// Now that the result of the task execution has been handled,
// throw the exception again, if any
if ($failure instanceof \Throwable) {
throw $failure;
}
}
return $result;
}
/**
* This method stores information about the last run of the Scheduler into the system registry
*
* @param string $type Type of run (manual or command-line (assumed to be cron))
*/
public function recordLastRun($type = 'cron')
{
// Validate input value
if ($type !== 'manual' && $type !== 'cli-by-id') {
$type = 'cron';
}
/** @var Registry $registry */
$registry = GeneralUtility::makeInstance(Registry::class);
$runInformation = ['start' => $GLOBALS['EXEC_TIME'], 'end' => time(), 'type' => $type];
$registry->set('tx_scheduler', 'lastRun', $runInformation);
}
/**
* Removes a task completely from the system.
*
* @todo find a way to actually kill the existing jobs
*
* @param Task\AbstractTask $task The object representing the task to delete
* @return bool TRUE if task was successfully deleted, FALSE otherwise
*/
public function removeTask(Task\AbstractTask $task)
{
$taskUid = $task->getTaskUid();
if (!empty($taskUid)) {
$result = GeneralUtility::makeInstance(ConnectionPool::class)
->getConnectionForTable('tx_scheduler_task')
->update('tx_scheduler_task', ['deleted' => 1], ['uid' => $taskUid]);
} else {
$result = false;
}
return $result;
}
/**
* Updates a task in the pool
*
* @param Task\AbstractTask $task Scheduler task object
* @return bool False if submitted task was not of proper class
*/
public function saveTask(Task\AbstractTask $task)
{
$result = true;
$taskUid = $task->getTaskUid();
if (!empty($taskUid)) {
try {
if ($task->getRunOnNextCronJob()) {
$executionTime = time();
} else {
$executionTime = $task->getNextDueExecution();
}
$task->setExecutionTime($executionTime);
} catch (\Exception $e) {
$task->setDisabled(true);
$executionTime = 0;
}
$task->unsetScheduler();
$fields = [
'nextexecution' => $executionTime,
'disable' => (int)$task->isDisabled(),
'description' => $task->getDescription(),
'task_group' => $task->getTaskGroup(),
'serialized_task_object' => serialize($task)
];
try {
GeneralUtility::makeInstance(ConnectionPool::class)
->getConnectionForTable('tx_scheduler_task')
->update(
'tx_scheduler_task',
$fields,
['uid' => $taskUid],
['serialized_task_object' => Connection::PARAM_LOB]
);
} catch (\Doctrine\DBAL\DBALException $e) {
$result = false;
}
} else {
$result = false;
}
return $result;
}
/**
* Fetches and unserializes a task object from the db. If an uid is given the object
* with the uid is returned, else the object representing the next due task is returned.
* If there are no due tasks the method throws an exception.
*
* @param int $uid Primary key of a task
* @return Task\AbstractTask The fetched task object
* @throws \OutOfBoundsException
* @throws \UnexpectedValueException
*/
public function fetchTask($uid = 0)
{
$connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
$queryBuilder = $connectionPool->getQueryBuilderForTable('tx_scheduler_task');
$queryBuilder->select('t.uid', 't.serialized_task_object')
->from('tx_scheduler_task', 't')
->setMaxResults(1);
// Define where clause
// If no uid is given, take any non-disabled task which has a next execution time in the past
if (empty($uid)) {
$queryBuilder->getRestrictions()->removeAll();
$queryBuilder->leftJoin(
't',
'tx_scheduler_task_group',
'g',
$queryBuilder->expr()->eq('t.task_group', $queryBuilder->quoteIdentifier('g.uid'))
);
$queryBuilder->where(
$queryBuilder->expr()->eq('t.disable', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)),
$queryBuilder->expr()->neq('t.nextexecution', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)),
$queryBuilder->expr()->lte(
't.nextexecution',
$queryBuilder->createNamedParameter($GLOBALS['EXEC_TIME'], \PDO::PARAM_INT)
),
$queryBuilder->expr()->orX(
$queryBuilder->expr()->eq('g.hidden', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)),
$queryBuilder->expr()->isNull('g.hidden')
),
$queryBuilder->expr()->eq('t.deleted', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT))
);
$queryBuilder->orderBy('t.nextexecution', 'ASC');
} else {
$queryBuilder->where(
$queryBuilder->expr()->eq('t.uid', $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)),
$queryBuilder->expr()->eq('t.deleted', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT))
);
}
$row = $queryBuilder->execute()->fetch();
if (empty($row)) {
if (empty($uid)) {
// No uid was passed and no overdue task was found
throw new \OutOfBoundsException('No tasks available for execution', 1247827244);
}
// Although a uid was passed, no task with given was found
throw new \OutOfBoundsException('No task with id ' . $uid . ' found', 1422044826);
}
/** @var Task\AbstractTask $task */
$task = unserialize($row['serialized_task_object']);
if ($this->isValidTaskObject($task)) {
// The task is valid, return it
$task->setScheduler();
} else {
// Forcibly set the disable flag to 1 in the database,
// so that the task does not come up again and again for execution
$connectionPool->getConnectionForTable('tx_scheduler_task')->update(
'tx_scheduler_task',
['disable' => 1],
['uid' => (int)$row['uid']]
);
// Throw an exception to raise the problem
throw new \UnexpectedValueException('Could not unserialize task', 1255083671);
}
return $task;
}
/**
* This method is used to get the database record for a given task
* It returns the database record and not the task object
*
* @param int $uid Primary key of the task to get
* @return array Database record for the task
* @see \TYPO3\CMS\Scheduler\Scheduler::fetchTask()
* @throws \OutOfBoundsException
*/
public function fetchTaskRecord($uid)
{
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable('tx_scheduler_task');
$row = $queryBuilder->select('*')
->from('tx_scheduler_task')
->where(
$queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter((int)$uid, \PDO::PARAM_INT)),
$queryBuilder->expr()->eq('deleted', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT))
)
->execute()
->fetch();
// If the task is not found, throw an exception
if (empty($row)) {
throw new \OutOfBoundsException('No task', 1247827245);
}
return $row;
}
/**
* Fetches and unserializes task objects selected with some (SQL) condition
* Objects are returned as an array
*
* @param string $where Part of a SQL where clause (without the "WHERE" keyword)
* @param bool $includeDisabledTasks TRUE if disabled tasks should be fetched too, FALSE otherwise
* @return array List of task objects
*/
public function fetchTasksWithCondition($where, $includeDisabledTasks = false)
{
$tasks = [];
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable('tx_scheduler_task');
$queryBuilder
->select('serialized_task_object')
->from('tx_scheduler_task')
->where(
$queryBuilder->expr()->eq('deleted', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT))
);
if (!$includeDisabledTasks) {
$queryBuilder->andWhere(
$queryBuilder->expr()->eq('disable', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT))
);
}
if (!empty($where)) {
$queryBuilder->andWhere(QueryHelper::stripLogicalOperatorPrefix($where));
}
$result = $queryBuilder->execute();
while ($row = $result->fetch()) {
/** @var Task\AbstractTask $task */
$task = unserialize($row['serialized_task_object']);
// Add the task to the list only if it is valid
if ($this->isValidTaskObject($task)) {
$task->setScheduler();
$tasks[] = $task;
}
}
return $tasks;
}
/**
* This method encapsulates a very simple test for the purpose of clarity.
* Registered tasks are stored in the database along with a serialized task object.
* When a registered task is fetched, its object is unserialized.
* At that point, if the class corresponding to the object is not available anymore
* (e.g. because the extension providing it has been uninstalled),
* the unserialization will produce an incomplete object.
* This test checks whether the unserialized object is of the right (parent) class or not.
*
* @param object $task The object to test
* @return bool TRUE if object is a task, FALSE otherwise
*/
public function isValidTaskObject($task)
{
return $task instanceof Task\AbstractTask && get_class($task->getExecution()) !== '__PHP_Incomplete_Class';
}
/**
* This is a utility method that writes some message to the BE Log
* It could be expanded to write to some other log
*
* @param string $message The message to write to the log
* @param int $status Status (0 = message, 1 = error)
* @param mixed $code Key for the message
*/
public function log($message, $status = 0, $code = '')
{
// this method could be called from the constructor (via "cleanExecutionArrays") and no logger is instantiated
// by then, that's why check if the logger is available
if (!($this->logger instanceof LoggerInterface)) {
$this->setLogger(GeneralUtility::makeInstance(LogManager::class)->getLogger(__CLASS__));
}
$message = trim('[scheduler]: ' . $code) . ' - ' . $message;
switch ((int)$status) {
// error (user problem)
case 1:
$this->logger->alert($message);
break;
// System Error (which should not happen)
case 2:
$this->logger->error($message);
break;
// security notice (admin)
case 3:
$this->logger->emergency($message);
break;
// regular message (= 0)
default:
$this->logger->info($message);
}
}
}
@@ -0,0 +1,606 @@
<?php
namespace TYPO3\CMS\Scheduler\Task;
/*
* 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!
*/
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use TYPO3\CMS\Core\Core\Environment;
use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Log\LogManager;
use TYPO3\CMS\Core\Utility\GeneralUtility;
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;
const TYPE_SINGLE = 1;
const TYPE_RECURRING = 2;
/**
* Reference to a scheduler object
*
* @var \TYPO3\CMS\Scheduler\Scheduler
*/
protected $scheduler;
/**
* The unique id of the task used to identify it in the database
*
* @var int
*/
protected $taskUid;
/**
* 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
*
* @var string
*/
protected $description = '';
/**
* Task group for this task
*
* @var int
*/
protected $taskGroup;
/**
* Constructor
*/
public function __construct()
{
// Using makeInstance instead of setScheduler() here as the logger is injected due to LoggerAwareTrait
$this->scheduler = GeneralUtility::makeInstance(\TYPO3\CMS\Scheduler\Scheduler::class);
$this->execution = GeneralUtility::makeInstance(Execution::class);
}
/**
* 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)
{
$this->taskUid = (int)$id;
}
/**
* This method returns the unique id of the task
*
* @return int The id of the task
*/
public function getTaskUid()
{
return $this->taskUid;
}
/**
* This method returns the title of the scheduler task
*
* @return string
*/
public function getTaskTitle()
{
return $GLOBALS['LANG']->sL($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][static::class]['title']);
}
/**
* This method returns the description of the scheduler task
*
* @return string
*/
public function getTaskDescription()
{
return $GLOBALS['LANG']->sL($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][static::class]['description']);
}
/**
* This method returns the class name of the scheduler task
*
* @return string
*/
public function getTaskClassName()
{
return static::class;
}
/**
* This method returns the disable 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 disable 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 Uid of task group
*/
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)
{
$this->description = $description;
}
/**
* This method returns the description of the task
*
* @return string Description
*/
public function getDescription()
{
return $this->description;
}
/**
* Sets the internal reference to the singleton instance of the Scheduler
* and the logger instance in case it was unserialized
*/
public function setScheduler()
{
$this->scheduler = GeneralUtility::makeInstance(\TYPO3\CMS\Scheduler\Scheduler::class);
$this->logger = GeneralUtility::makeInstance(LogManager::class)->getLogger(__CLASS__);
}
/**
* Unsets the internal reference to the singleton instance of the Scheduler
* and the logger instance.
* This is done before a task is serialized, so that the scheduler instance
* and the logger instance are not saved to the database
*/
public function unsetScheduler()
{
$this->scheduler = null;
$this->logger = null;
}
/**
* Registers a single execution of the task
*
* @param int $timestamp Timestamp of the next execution
*/
public function registerSingleExecution($timestamp)
{
/** @var Execution $execution */
$execution = GeneralUtility::makeInstance(Execution::class);
$execution->setStart($timestamp);
$execution->setInterval(0);
$execution->setEnd($timestamp);
$execution->setCronCmd('');
$execution->setMultiple(0);
$execution->setIsNewSingleExecution(true);
// Replace existing execution object
$this->execution = $execution;
}
/**
* Registers a recurring execution of the task
*
* @param int $start The first date/time where this execution should occur (timestamp)
* @param string $interval Execution interval in seconds
* @param int $end The last date/time where this execution should occur (timestamp)
* @param bool $multiple Set to FALSE if multiple executions of this task are not permitted in parallel
* @param string $cron_cmd Used like in crontab (minute hour day month weekday)
*/
public function registerRecurringExecution($start, $interval, $end = 0, $multiple = false, $cron_cmd = '')
{
/** @var Execution $execution */
$execution = GeneralUtility::makeInstance(Execution::class);
// Set general values
$execution->setStart($start);
$execution->setEnd($end);
$execution->setMultiple($multiple);
if (empty($cron_cmd)) {
// Use interval
$execution->setInterval($interval);
$execution->setCronCmd('');
} else {
// Use cron syntax
$execution->setInterval(0);
$execution->setCronCmd($cron_cmd);
}
// Replace existing execution object
$this->execution = $execution;
}
/**
* Sets the internal execution object
*
* @param Execution $execution The execution to add
*/
public function setExecution(Execution $execution)
{
$this->execution = $execution;
}
/**
* Returns the execution object
*
* @return Execution The internal execution object
*/
public function getExecution()
{
return $this->execution;
}
/**
* Returns the timestamp for next due execution of the task
*
* @return int Date and time of the next execution as a timestamp
*/
public function getNextDueExecution()
{
// NOTE: this call may throw an exception, but we let it bubble up
return $this->execution->getNextExecution();
}
/**
* Returns TRUE if several runs of the task are allowed concurrently
*
* @return bool TRUE if concurrent executions are allowed, FALSE otherwise
*/
public function areMultipleExecutionsAllowed()
{
return $this->execution->getMultiple();
}
/**
* Returns TRUE if an instance of the task is already running
*
* @return bool TRUE if an instance is already running, FALSE otherwise
*/
public function isExecutionRunning()
{
$isRunning = false;
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable('tx_scheduler_task');
$row = $queryBuilder
->select('serialized_executions')
->from('tx_scheduler_task')
->where(
$queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($this->taskUid, \PDO::PARAM_INT))
)
->execute()
->fetch();
if ($row && !empty($row['serialized_executions'])) {
$isRunning = true;
}
return $isRunning;
}
/**
* This method adds current execution to the execution list
* It also logs the execution time and mode
*
* @return int Execution id
*/
public function markExecution()
{
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable('tx_scheduler_task');
$row = $queryBuilder
->select('serialized_executions')
->from('tx_scheduler_task')
->where(
$queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($this->taskUid, \PDO::PARAM_INT))
)
->execute()
->fetch();
$runningExecutions = [];
if ($row && !empty($row['serialized_executions'])) {
$runningExecutions = unserialize($row['serialized_executions']);
}
// Count the number of existing executions and use that number as a key
// (we need to know that number, because it is returned at the end of the method)
$numExecutions = count($runningExecutions);
$runningExecutions[$numExecutions] = time();
GeneralUtility::makeInstance(ConnectionPool::class)
->getConnectionForTable('tx_scheduler_task')
->update(
'tx_scheduler_task',
[
'serialized_executions' => serialize($runningExecutions),
'lastexecution_time' => time(),
// Define the context in which the script is running
'lastexecution_context' => Environment::isCli() ? 'CLI' : 'BE'
],
[
'uid' => $this->taskUid
],
[
'serialized_executions' => Connection::PARAM_LOB,
]
);
return $numExecutions;
}
/**
* Removes given execution from list
*
* @param int $executionID Id of the execution to remove.
* @param \Throwable $failure An exception to signal a failed execution
*/
public function unmarkExecution($executionID, \Throwable $failure = null)
{
// Get the executions for the task
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable('tx_scheduler_task');
$row = $queryBuilder
->select('serialized_executions')
->from('tx_scheduler_task')
->where(
$queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($this->taskUid, \PDO::PARAM_INT))
)
->execute()
->fetch();
if ($row && $row['serialized_executions'] !== '') {
$runningExecutions = unserialize($row['serialized_executions']);
// Remove the selected execution
unset($runningExecutions[$executionID]);
if (!empty($runningExecutions)) {
// Re-serialize the updated executions list (if necessary)
$runningExecutionsSerialized = serialize($runningExecutions);
} else {
$runningExecutionsSerialized = '';
}
if ($failure instanceof \Throwable) {
// Log failed execution
$logMessage = 'Task failed to execute successfully. Class: ' . static::class
. ', UID: ' . $this->taskUid . ', Code: ' . $failure->getCode() . ', ' . $failure->getMessage();
$this->logger->error($logMessage, ['exception' => $failure]);
// Do not serialize the complete exception or the trace, this can lead to huge strings > 50MB
$failureString = serialize([
'code' => $failure->getCode(),
'message' => $failure->getMessage(),
'file' => $failure->getFile(),
'line' => $failure->getLine(),
'traceString' => $failure->getTraceAsString(),
]);
} else {
$failureString = '';
}
// Save the updated executions list
GeneralUtility::makeInstance(ConnectionPool::class)
->getConnectionForTable('tx_scheduler_task')
->update(
'tx_scheduler_task',
[
'serialized_executions' => $runningExecutionsSerialized,
'lastexecution_failure' => $failureString
],
[
'uid' => $this->taskUid
],
[
'serialized_executions' => Connection::PARAM_LOB,
]
);
}
}
/**
* Clears all marked executions
*
* @return bool TRUE if the clearing succeeded, FALSE otherwise
*/
public function unmarkAllExecutions()
{
// Set the serialized executions field to empty
$result = GeneralUtility::makeInstance(ConnectionPool::class)
->getConnectionForTable('tx_scheduler_task')
->update(
'tx_scheduler_task',
[
'serialized_executions' => ''
],
[
'uid' => $this->taskUid
],
[
'serialized_executions' => Connection::PARAM_LOB,
]
);
return (bool)$result;
}
/**
* Saves the details of the task to the database.
*
* @return bool
*/
public function save()
{
return $this->scheduler->saveTask($this);
}
/**
* Stops the task, by replacing the execution object by an empty one
* NOTE: the task still needs to be saved after that
*/
public function stop()
{
$this->execution = GeneralUtility::makeInstance(Execution::class);
}
/**
* Removes the task totally from the system.
*/
public function remove()
{
$this->scheduler->removeTask($this);
}
/**
* Guess task type from the existing information
* If an interval or a cron command is defined, it's a recurring task
*
* @return int
*/
public function getType()
{
if (!empty($this->getExecution()->getInterval()) || !empty($this->getExecution()->getCronCmd())) {
return self::TYPE_RECURRING;
}
return self::TYPE_SINGLE;
}
/**
* @param \Exception $e
*/
protected function logException(\Exception $e)
{
$this->logger->error('A Task Exception was captured: ' . $e->getMessage() . ' (' . $e->getCode() . ')', ['exception' => $e]);
}
}
@@ -0,0 +1,143 @@
<?php
namespace TYPO3\CMS\Scheduler\Task;
/*
* 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!
*/
use TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend;
use TYPO3\CMS\Core\Messaging\FlashMessage;
use TYPO3\CMS\Scheduler\AbstractAdditionalFieldProvider;
use TYPO3\CMS\Scheduler\Controller\SchedulerModuleController;
use TYPO3\CMS\Scheduler\Task\Enumeration\Action;
/**
* Additional BE fields for caching framework garbage collection task.
* Creates a multi selectbox with all available cache backends to select from.
* @internal This class is a specific scheduler task implementation is not considered part of the Public TYPO3 API.
*/
class CachingFrameworkGarbageCollectionAdditionalFieldProvider extends AbstractAdditionalFieldProvider
{
/**
* Add a multi select box with all available cache backends.
*
* @param array $taskInfo Reference to the array containing the info used in the add/edit form
* @param AbstractTask|null $task When editing, reference to the current task. NULL when adding.
* @param SchedulerModuleController $schedulerModule Reference to the calling object (Scheduler's BE module)
* @return array Array containing all the information pertaining to the additional fields
*/
public function getAdditionalFields(array &$taskInfo, $task, SchedulerModuleController $schedulerModule)
{
$currentSchedulerModuleAction = $schedulerModule->getCurrentAction();
// Initialize selected fields
if (empty($taskInfo['scheduler_cachingFrameworkGarbageCollection_selectedBackends'])) {
$taskInfo['scheduler_cachingFrameworkGarbageCollection_selectedBackends'] = [];
if ($currentSchedulerModuleAction->equals(Action::ADD)) {
// In case of new task, set to dbBackend if it's available
if (in_array(Typo3DatabaseBackend::class, $this->getRegisteredBackends())) {
$taskInfo['scheduler_cachingFrameworkGarbageCollection_selectedBackends'][] = Typo3DatabaseBackend::class;
}
} elseif ($currentSchedulerModuleAction->equals(Action::EDIT)) {
// In case of editing the task, set to currently selected value
$taskInfo['scheduler_cachingFrameworkGarbageCollection_selectedBackends'] = $task->selectedBackends;
}
}
$fieldName = 'tx_scheduler[scheduler_cachingFrameworkGarbageCollection_selectedBackends][]';
$fieldId = 'task_cachingFrameworkGarbageCollection_selectedBackends';
$fieldOptions = $this->getCacheBackendOptions($taskInfo['scheduler_cachingFrameworkGarbageCollection_selectedBackends']);
$fieldHtml = '<select class="form-control" name="' . $fieldName . '" id="' . $fieldId . '" class="from-control" size="10" multiple="multiple">' . $fieldOptions . '</select>';
$additionalFields[$fieldId] = [
'code' => $fieldHtml,
'label' => 'LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:label.cachingFrameworkGarbageCollection.selectBackends',
'cshKey' => '_MOD_system_txschedulerM1',
'cshLabel' => $fieldId
];
return $additionalFields;
}
/**
* Checks that all selected backends exist in available backend list
*
* @param array $submittedData Reference to the array containing the data submitted by the user
* @param SchedulerModuleController $schedulerModule Reference to the calling object (Scheduler's BE module)
* @return bool TRUE if validation was ok (or selected class is not relevant), FALSE otherwise
*/
public function validateAdditionalFields(array &$submittedData, SchedulerModuleController $schedulerModule)
{
$validData = true;
$availableBackends = $this->getRegisteredBackends();
if (is_array($submittedData['scheduler_cachingFrameworkGarbageCollection_selectedBackends'])) {
$invalidBackends = array_diff($submittedData['scheduler_cachingFrameworkGarbageCollection_selectedBackends'], $availableBackends);
if (!empty($invalidBackends)) {
$this->addMessage($GLOBALS['LANG']->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:msg.selectionOfNonExistingCacheBackends'), FlashMessage::ERROR);
$validData = false;
}
} else {
$this->addMessage($GLOBALS['LANG']->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:msg.noCacheBackendSelected'), FlashMessage::ERROR);
$validData = false;
}
return $validData;
}
/**
* Save selected backends in task object
*
* @param array $submittedData Contains data submitted by the user
* @param \TYPO3\CMS\Scheduler\Task\AbstractTask $task Reference to the current task object
*/
public function saveAdditionalFields(array $submittedData, AbstractTask $task)
{
$task->selectedBackends = $submittedData['scheduler_cachingFrameworkGarbageCollection_selectedBackends'];
}
/**
* Build select options of available backends and set currently selected backends
*
* @param array $selectedBackends Selected backends
* @return string HTML of selectbox options
*/
protected function getCacheBackendOptions(array $selectedBackends)
{
$options = [];
$availableBackends = $this->getRegisteredBackends();
foreach ($availableBackends as $backendName) {
if (in_array($backendName, $selectedBackends)) {
$selected = ' selected="selected"';
} else {
$selected = '';
}
$options[] = '<option value="' . $backendName . '"' . $selected . '>' . $backendName . '</option>';
}
return implode('', $options);
}
/**
* Get all registered caching framework backends
*
* @return array Registered backends
*/
protected function getRegisteredBackends()
{
$backends = [];
$cacheConfigurations = $GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations'];
if (is_array($cacheConfigurations)) {
foreach ($cacheConfigurations as $cacheConfiguration) {
$backend = $cacheConfiguration['backend'];
if (!in_array($backend, $backends)) {
$backends[] = $backend;
}
}
}
return $backends;
}
}
@@ -0,0 +1,57 @@
<?php
namespace TYPO3\CMS\Scheduler\Task;
/*
* 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!
*/
/**
* 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 \TYPO3\CMS\Scheduler\Task\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'];
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'];
if (in_array($usedCacheBackend, $this->selectedBackends)) {
\TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Core\Cache\CacheManager::class)->getCache($cacheName)->collectGarbage();
}
}
}
return true;
}
}
@@ -0,0 +1,36 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Scheduler\Task\Enumeration;
/*
* 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!
*/
use TYPO3\CMS\Core\Type\Enumeration;
/**
* List of possible scheduler actions
*/
final class Action extends Enumeration
{
const __default = self::LIST;
const ADD = 'add';
const DELETE = 'delete';
const EDIT = 'edit';
const LIST = 'list';
const SAVE = 'save';
const SAVE_CLOSE = 'saveclose';
const SAVE_NEW = 'savenew';
const SET_NEXT_EXECUTION_TIME = 'setNextExecutionTime';
const STOP = 'stop';
const TOGGLE_HIDDEN = 'toggleHidden';
}
@@ -0,0 +1,451 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Scheduler\Task;
/*
* 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!
*/
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputDefinition;
use Symfony\Component\Console\Input\InputOption;
use TYPO3\CMS\Core\Console\CommandRegistry;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Messaging\FlashMessage;
use TYPO3\CMS\Core\Messaging\FlashMessageService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Scheduler\AdditionalFieldProviderInterface;
use TYPO3\CMS\Scheduler\Controller\SchedulerModuleController;
use TYPO3Fluid\Fluid\Core\ViewHelper\TagBuilder;
/**
* @internal This class is a specific scheduler task implementation is not considered part of the Public TYPO3 API.
*/
class ExecuteSchedulableCommandAdditionalFieldProvider implements AdditionalFieldProviderInterface
{
/**
* @var Command[]
*/
protected $schedulableCommands = [];
/**
* @var \TYPO3\CMS\Extbase\Object\ObjectManagerInterface
*/
protected $objectManager;
/**
* @var \TYPO3\CMS\Extbase\Reflection\ReflectionService
*/
protected $reflectionService;
/**
* @var ExecuteSchedulableCommandTask
*/
protected $task;
public function __construct()
{
$commandRegistry = GeneralUtility::makeInstance(CommandRegistry::class);
foreach ($commandRegistry->getSchedulableCommands() as $commandIdentifier => $command) {
$this->schedulableCommands[$commandIdentifier] = $command;
}
ksort($this->schedulableCommands);
}
/**
* Render additional information fields within the scheduler backend.
*
* @param array &$taskInfo Array information of task to return
* @param AbstractTask|null $task When editing, reference to the current task. NULL when adding.
* @param SchedulerModuleController $schedulerModule Reference to the calling object (BE module of the Scheduler)
* @return array Additional fields
* @see \TYPO3\CMS\Scheduler\AdditionalFieldProvider#getAdditionalFields($taskInfo, $task, $schedulerModule)
*/
public function getAdditionalFields(array &$taskInfo, $task, SchedulerModuleController $schedulerModule): array
{
$this->task = $task;
if ($this->task !== null) {
$this->task->setScheduler();
}
$fields = [];
$fields['action'] = $this->getActionField();
if ($this->task !== null && isset($this->schedulableCommands[$this->task->getCommandIdentifier()])) {
$command = $this->schedulableCommands[$this->task->getCommandIdentifier()];
$fields['description'] = $this->getCommandDescriptionField($command->getDescription());
$argumentFields = $this->getCommandArgumentFields($command->getDefinition());
$fields = array_merge($fields, $argumentFields);
$optionFields = $this->getCommandOptionFields($command->getDefinition());
$fields = array_merge($fields, $optionFields);
$this->task->save(); // todo: this seems to be superfluous
}
return $fields;
}
/**
* Validates additional selected fields
*
* @param array &$submittedData
* @param SchedulerModuleController $schedulerModule
* @return bool
*/
public function validateAdditionalFields(array &$submittedData, SchedulerModuleController $schedulerModule): bool
{
if (!isset($this->schedulableCommands[$submittedData['task_executeschedulablecommand']['command']])) {
return false;
}
$command = $this->schedulableCommands[$submittedData['task_executeschedulablecommand']['command']];
/** @var FlashMessageService $flashMessageService */
$flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
$hasErrors = false;
foreach ($command->getDefinition()->getArguments() as $argument) {
foreach ((array)$submittedData['task_executeschedulablecommand']['arguments'] as $argumentName => $argumentValue) {
/** @var string $argumentName */
/** @var string $argumentValue */
if ($argument->getName() !== $argumentName) {
continue;
}
if ($argument->isRequired() && trim($argumentValue) === '') {
// Argument is required and argument value is empty0
$flashMessageService->getMessageQueueByIdentifier()->addMessage(
new FlashMessage(
sprintf(
$this->getLanguageService()->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:msg.mandatoryArgumentMissing'),
$argumentName
),
$this->getLanguageService()->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:msg.updateError'),
FlashMessage::ERROR
)
);
$hasErrors = true;
}
}
}
foreach ($command->getDefinition()->getOptions() as $optionDefinition) {
$optionEnabled = $submittedData['task_executeschedulablecommand']['options'][$optionDefinition->getName()] ?? false;
$optionValue = $submittedData['task_executeschedulablecommand']['option_values'][$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
$flashMessageService->getMessageQueueByIdentifier()->addMessage(
new FlashMessage(
sprintf(
$this->getLanguageService()->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:msg.mandatoryArgumentMissing'),
$optionDefinition->getName()
),
$this->getLanguageService()->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:msg.updateError'),
FlashMessage::ERROR
)
);
$hasErrors = true;
}
}
}
}
return $hasErrors === false;
}
/**
* Saves additional field values
*
* @param array $submittedData
* @param AbstractTask $task
* @return bool
*/
public function saveAdditionalFields(array $submittedData, AbstractTask $task): bool
{
$command = $this->schedulableCommands[$submittedData['task_executeschedulablecommand']['command']];
/** @var ExecuteSchedulableCommandTask $task */
$task->setCommandIdentifier($submittedData['task_executeschedulablecommand']['command']);
$arguments = [];
foreach ((array)$submittedData['task_executeschedulablecommand']['arguments'] as $argumentName => $argumentValue) {
try {
$argumentDefinition = $command->getDefinition()->getArgument($argumentName);
} catch (InvalidArgumentException $e) {
continue;
}
if ($argumentDefinition->isArray()) {
$argumentValue = GeneralUtility::trimExplode(',', $argumentValue, true);
}
$arguments[$argumentName] = $argumentValue;
}
$options = [];
$optionValues = [];
foreach ($command->getDefinition()->getOptions() as $optionDefinition) {
$optionEnabled = $submittedData['task_executeschedulablecommand']['options'][$optionDefinition->getName()] ?? false;
$options[$optionDefinition->getName()] = (bool)$optionEnabled;
if ($optionDefinition->isValueRequired() || $optionDefinition->isValueOptional() || $optionDefinition->isArray()) {
$optionValue = $submittedData['task_executeschedulablecommand']['option_values'][$optionDefinition->getName()] ?? $optionDefinition->getDefault();
if ($optionDefinition->isArray() && !is_array($optionValue)) {
// Do not remove empty array values.
// One empty array element indicates the existence of one occurence 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, false);
}
} else {
// boolean flag: option value must be true if option is added or false otherwise
$optionValue = (bool)$optionEnabled;
}
$optionValues[$optionDefinition->getName()] = $optionValue;
}
$task->setArguments($arguments);
$task->setOptions($options);
$task->setOptionValues($optionValues);
return true;
}
/**
* Get description of selected command
*
* @param string $description
* @return array
*/
protected function getCommandDescriptionField(string $description): array
{
return [
'code' => '',
'label' => '<strong>' . $description . '</strong>'
];
}
/**
* Gets a select field containing all possible schedulable commands
*
* @return array
*/
protected function getActionField(): array
{
$currentlySelectedCommand = $this->task !== null ? $this->task->getCommandIdentifier() : '';
$options = [];
foreach ($this->schedulableCommands as $commandIdentifier => $command) {
$options[$commandIdentifier] = $commandIdentifier . ': ' . $command->getDescription();
}
return [
'code' => $this->renderSelectField($options, $currentlySelectedCommand),
'label' => $this->getLanguageService()->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:label.schedulableCommandName')
];
}
/**
* Gets a set of fields covering arguments which can or must be used.
* Also registers the default values of those fields with the Task, allowing
* them to be read upon execution.
*
* @param InputDefinition $inputDefinition
* @return array
*/
protected function getCommandArgumentFields(InputDefinition $inputDefinition): array
{
$fields = [];
$argumentValues = $this->task->getArguments();
foreach ($inputDefinition->getArguments() as $argument) {
$name = $argument->getName();
$defaultValue = $argument->getDefault();
$this->task->addDefaultValue($name, $defaultValue);
$value = $argumentValues[$name] ?? $defaultValue;
if (is_array($value) && $argument->isArray()) {
$value = implode(',', $value);
}
$fields[$name] = [
'code' => $this->renderArgumentField($argument, (string)$value),
'label' => $this->getArgumentLabel($argument)
];
}
return $fields;
}
/**
* Gets a set of fields covering options which can or must be used.
* Also registers the default values of those fields with the Task, allowing
* them to be read upon execution.
*
* @param InputDefinition $inputDefinition
* @return array
*/
protected function getCommandOptionFields(InputDefinition $inputDefinition): array
{
$fields = [];
$enabledOptions = $this->task->getOptions();
$optionValues = $this->task->getOptionValues();
foreach ($inputDefinition->getOptions() as $option) {
$name = $option->getName();
$defaultValue = $option->getDefault();
$this->task->addDefaultValue($name, $defaultValue);
$enabled = $enabledOptions[$name] ?? false;
$value = $optionValues[$name] ?? $defaultValue;
if (is_array($value) && $option->isArray()) {
$value = implode(',', $value);
}
$fields[$name] = [
'code' => $this->renderOptionField($option, (bool)$enabled, (string)$value),
'label' => $this->getOptionLabel($option)
];
}
return $fields;
}
/**
* Get a human-readable label for a command argument
*
* @param InputArgument $argument
* @return string
*/
protected function getArgumentLabel(InputArgument $argument): string
{
return 'Argument: ' . $argument->getName() . '. <em>' . htmlspecialchars($argument->getDescription()) . '</em>';
}
/**
* Get a human-readable label for a command option
*
* @param InputOption $option
* @return string
*/
protected function getOptionLabel(InputOption $option): string
{
return 'Option: ' . htmlspecialchars($option->getName()) . '. <em>' . htmlspecialchars($option->getDescription()) . '</em>';
}
/**
* @param array $options
* @param string $selectedOptionValue
* @return string
*/
protected function renderSelectField(array $options, string $selectedOptionValue): string
{
$selectTag = new TagBuilder();
$selectTag->setTagName('select');
$selectTag->forceClosingTag(true);
$selectTag->addAttribute('class', 'form-control');
$selectTag->addAttribute('name', 'tx_scheduler[task_executeschedulablecommand][command]');
$optionsHtml = '';
foreach ($options as $value => $label) {
$optionTag = new TagBuilder();
$optionTag->setTagName('option');
$optionTag->forceClosingTag(true);
$optionTag->addAttribute('title', (string)$label);
$optionTag->addAttribute('value', (string)$value);
$optionTag->setContent($label);
if ($value === $selectedOptionValue) {
$optionTag->addAttribute('selected', 'selected');
}
$optionsHtml .= $optionTag->render();
}
$selectTag->setContent($optionsHtml);
return $selectTag->render();
}
/**
* Renders a field for defining an argument's value
*
* @param InputArgument $argument
* @param mixed $currentValue
* @return string
*/
protected function renderArgumentField(InputArgument $argument, string $currentValue): string
{
$name = $argument->getName();
$fieldName = 'tx_scheduler[task_executeschedulablecommand][arguments][' . $name . ']';
$inputTag = new TagBuilder();
$inputTag->setTagName('input');
$inputTag->addAttribute('type', 'text');
$inputTag->addAttribute('name', $fieldName);
$inputTag->addAttribute('value', $currentValue);
$inputTag->addAttribute('class', 'form-control');
return $inputTag->render();
}
/**
* Renders a field for defining an option's value
*
* @param InputOption $option
* @param mixed $currentValue
* @return string
*/
protected function renderOptionField(InputOption $option, bool $enabled, string $currentValue): string
{
$name = $option->getName();
$checkboxFieldName = 'tx_scheduler[task_executeschedulablecommand][options][' . $name . ']';
$checkboxId = 'tx_scheduler_task_executeschedulablecommand_options_' . $name;
$checkboxTag = new TagBuilder();
$checkboxTag->setTagName('input');
$checkboxTag->addAttribute('id', $checkboxId);
$checkboxTag->addAttribute('name', $checkboxFieldName);
$checkboxTag->addAttribute('type', 'checkbox');
if ($enabled) {
$checkboxTag->addAttribute('checked', 'checked');
}
$html = '<label for="' . $checkboxId . '">'
. $checkboxTag->render()
. ' ' . $this->getLanguageService()->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:label.addOptionToCommand')
. '</label>';
if ($option->isValueRequired() || $option->isValueOptional() || $option->isArray()) {
$valueFieldName = 'tx_scheduler[task_executeschedulablecommand][option_values][' . $name . ']';
$inputTag = new TagBuilder();
$inputTag->setTagName('input');
$inputTag->addAttribute('name', $valueFieldName);
$inputTag->addAttribute('type', 'text');
$inputTag->addAttribute('value', $currentValue);
$inputTag->addAttribute('class', 'form-control');
$html .= $inputTag->render();
}
return $html;
}
/**
* @return LanguageService
*/
public function getLanguageService(): LanguageService
{
return $GLOBALS['LANG'];
}
}
@@ -0,0 +1,208 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Scheduler\Task;
/*
* 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!
*/
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\Console\UnknownCommandException;
use TYPO3\CMS\Core\Localization\LanguageService;
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 = [];
/**
* @param string $commandIdentifier
*/
public function setCommandIdentifier(string $commandIdentifier)
{
$this->commandIdentifier = $commandIdentifier;
}
/**
* @return string
*/
public function getCommandIdentifier(): string
{
return $this->commandIdentifier;
}
/**
* 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->getCommandByIdentifier($this->commandIdentifier);
} catch (UnknownCommandException $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));
$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
{
$label = $this->commandIdentifier;
try {
$commandRegistry = GeneralUtility::makeInstance(CommandRegistry::class);
$schedulableCommand = $commandRegistry->getCommandByIdentifier($this->commandIdentifier);
} catch (UnknownCommandException $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|\Symfony\Component\Console\Exception\InvalidArgumentException $e) {
return $label . "\n"
. sprintf(
$this->getLanguageService()->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:msg.errorParsingArguments'),
$e->getMessage()
);
} catch (InvalidOptionException $e) {
return $label . "\n"
. sprintf(
$this->getLanguageService()->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:msg.errorParsingOptions'),
$e->getMessage()
);
}
if ($arguments !== '') {
$label .= ' ' . $arguments;
}
return $label;
}
public function getArguments(): array
{
return $this->arguments;
}
public function setArguments(array $arguments)
{
$this->arguments = $arguments;
}
public function getOptions(): array
{
return $this->options;
}
public function setOptions(array $options)
{
$this->options = $options;
}
public function getOptionValues(): array
{
return $this->optionValues;
}
public function setOptionValues(array $optionValues)
{
$this->optionValues = $optionValues;
}
/**
* @param string $argumentName
* @param mixed $argumentValue
*/
public function addDefaultValue(string $argumentName, $argumentValue)
{
if (is_bool($argumentValue)) {
$argumentValue = (int)$argumentValue;
}
$this->defaults[$argumentName] = $argumentValue;
}
/**
* @return LanguageService
*/
public function getLanguageService(): LanguageService
{
return $GLOBALS['LANG'];
}
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);
}
}
@@ -0,0 +1,208 @@
<?php
namespace TYPO3\CMS\Scheduler\Task;
/*
* 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!
*/
use TYPO3\CMS\Core\Resource\Index\ExtractorInterface;
use TYPO3\CMS\Core\Resource\Index\ExtractorRegistry;
use TYPO3\CMS\Core\Resource\ResourceFactory;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
use TYPO3\CMS\Scheduler\AdditionalFieldProviderInterface;
use TYPO3\CMS\Scheduler\Controller\SchedulerModuleController;
/**
* Additional BE fields for task which extracts metadata from storage
* @internal This class is a specific scheduler task implementation is not considered part of the Public TYPO3 API.
*/
class FileStorageExtractionAdditionalFieldProvider implements AdditionalFieldProviderInterface
{
/**
* Add additional fields
*
* @param array $taskInfo Reference to the array containing the info used in the add/edit form
* @param AbstractTask|null $task When editing, reference to the current task. NULL when adding.
* @param SchedulerModuleController $parentObject Reference to the calling object (Scheduler's BE module)
* @return array Array containing all the information pertaining to the additional fields
* @throws \InvalidArgumentException
*/
public function getAdditionalFields(array &$taskInfo, $task, SchedulerModuleController $parentObject)
{
if ($task !== null && !$task instanceof FileStorageExtractionTask) {
throw new \InvalidArgumentException('Task not of type FileStorageExtractionTask', 1384275695);
}
$additionalFields['scheduler_fileStorageIndexing_storage'] = $this->getAllStoragesField($task);
$additionalFields['scheduler_fileStorageIndexing_fileCount'] = $this->getFileCountField($task);
$additionalFields['scheduler_fileStorageIndexing_registeredExtractors'] = $this->getRegisteredExtractorsField($task);
return $additionalFields;
}
/**
* Returns a field configuration including a selectbox for available storages
*
* @param FileStorageExtractionTask|null $task When editing, reference to the current task object. NULL when adding.
* @return array Array containing all the information pertaining to the additional fields
*/
protected function getAllStoragesField(FileStorageExtractionTask $task = null)
{
/** @var \TYPO3\CMS\Core\Resource\ResourceStorage[] $storages */
$storages = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Resource\StorageRepository::class)->findAll();
$options = [];
foreach ($storages as $storage) {
if ($task !== null && $task->storageUid === $storage->getUid()) {
$options[] = '<option value="' . $storage->getUid() . '" selected="selected">' . $storage->getName() . '</option>';
} else {
$options[] = '<option value="' . $storage->getUid() . '">' . $storage->getName() . '</option>';
}
}
$fieldName = 'tx_scheduler[scheduler_fileStorageIndexing_storage]';
$fieldId = 'scheduler_fileStorageIndexing_storage';
$fieldHtml = '<select class="form-control" name="' . $fieldName . '" id="' . $fieldId . '">' . implode("\n", $options) . '</select>';
$fieldConfiguration = [
'code' => $fieldHtml,
'label' => 'LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:label.fileStorageIndexing.storage',
'cshKey' => '_MOD_system_txschedulerM1',
'cshLabel' => $fieldId
];
return $fieldConfiguration;
}
/**
* Returns a field configuration including an input field for the file count
*
* @param FileStorageExtractionTask|null $task When editing, reference to the current task object. NULL when adding.
* @return array Array containing all the information pertaining to the additional fields
*/
protected function getFileCountField(FileStorageExtractionTask $task = null)
{
$fieldName = 'tx_scheduler[scheduler_fileStorageIndexing_fileCount]';
$fieldId = 'scheduler_fileStorageIndexing_fileCount';
$fieldValue = $task !== null ? (int)$task->maxFileCount : 100;
$fieldHtml = '<input type="text" class="form-control" name="' . $fieldName . '" id="' . $fieldId . '" value="' . htmlspecialchars($fieldValue) . '">';
$fieldConfiguration = [
'code' => $fieldHtml,
'label' => 'LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:label.fileStorageExtraction.fileCount',
'cshKey' => '_MOD_system_txschedulerM1',
'cshLabel' => $fieldId
];
return $fieldConfiguration;
}
/**
* Returns a field configuration telling about the status of registered extractors.
*
* @param FileStorageExtractionTask|null $task When editing, reference to the current task object. NULL when adding.
* @return array Array containing all the information pertaining to the additional fields
*/
protected function getRegisteredExtractorsField(FileStorageExtractionTask $task = null)
{
$extractors = ExtractorRegistry::getInstance()->getExtractors();
if (empty($extractors)) {
$labelKey = 'LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:label.fileStorageExtraction.registeredExtractors.without_extractors';
$content = '<span class="label label-warning">'
. htmlspecialchars($this->getLanguageService()->sL($labelKey))
. '</span>';
} else {
// Assemble the extractor bullet list first.
$labelKey = 'LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:label.fileStorageExtraction.registeredExtractors.extractor';
$bullets = [];
foreach ($extractors as $extractor) {
$bullets[] = sprintf(
'<li title="%s">%s</li>',
get_class($extractor),
sprintf($this->getLanguageService()->sL($labelKey), $this->formatExtractorClassName($extractor), $extractor->getPriority())
);
}
// Finalize content assembling.
$labelKey = 'LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:label.fileStorageExtraction.registeredExtractors.with_extractors';
$title = $this->getLanguageService()->sL($labelKey);
$content = '<p>' . htmlspecialchars($title) . '</p>';
$content .= '<ul>' . implode(LF, $bullets) . '</ul>';
}
$fieldConfiguration = [
'code' => $content,
'label' => 'LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:label.fileStorageExtraction.registeredExtractors',
'cshKey' => '_MOD_system_txschedulerM1',
'cshLabel' => 'scheduler_fileStorageIndexing_registeredExtractors'
];
return $fieldConfiguration;
}
/**
* Validate additional fields
*
* @param array $submittedData Reference to the array containing the data submitted by the user
* @param SchedulerModuleController $parentObject Reference to the calling object (Scheduler's BE module)
* @return bool True if validation was ok (or selected class is not relevant), false otherwise
*/
public function validateAdditionalFields(array &$submittedData, SchedulerModuleController $parentObject)
{
if (
!MathUtility::canBeInterpretedAsInteger($submittedData['scheduler_fileStorageIndexing_storage'])
|| !MathUtility::canBeInterpretedAsInteger($submittedData['scheduler_fileStorageIndexing_fileCount'])
) {
return false;
}
if (ResourceFactory::getInstance()->getStorageObject($submittedData['scheduler_fileStorageIndexing_storage']) === null) {
return false;
}
if (!MathUtility::isIntegerInRange($submittedData['scheduler_fileStorageIndexing_fileCount'], 1, 9999)) {
return false;
}
return true;
}
/**
* Save additional field in task
*
* @param array $submittedData Contains data submitted by the user
* @param AbstractTask $task Reference to the current task object
* @throws \InvalidArgumentException
*/
public function saveAdditionalFields(array $submittedData, AbstractTask $task)
{
if (!$task instanceof FileStorageExtractionTask) {
throw new \InvalidArgumentException('Task not of type FileStorageExtractionTask', 1384275698);
}
$task->storageUid = (int)$submittedData['scheduler_fileStorageIndexing_storage'];
$task->maxFileCount = (int)$submittedData['scheduler_fileStorageIndexing_fileCount'];
}
/**
* Since the class name can be very long considering the namespace, only take the final
* part for better readability. The FQN of the class will be displayed as tooltip.
*
* @param ExtractorInterface $extractor
* @return string
*/
protected function formatExtractorClassName(ExtractorInterface $extractor)
{
$extractorParts = explode('\\', get_class($extractor));
return array_pop($extractorParts);
}
/**
* @return \TYPO3\CMS\Core\Localization\LanguageService
*/
protected function getLanguageService()
{
return $GLOBALS['LANG'];
}
}
@@ -0,0 +1,72 @@
<?php
namespace TYPO3\CMS\Scheduler\Task;
/*
* 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!
*/
/**
* 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 \TYPO3\CMS\Scheduler\Task\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 = \TYPO3\CMS\Core\Resource\ResourceFactory::getInstance()->getStorageObject($this->storageUid);
$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;
}
/**
* Gets the indexer
*
* @param \TYPO3\CMS\Core\Resource\ResourceStorage $storage
* @return \TYPO3\CMS\Core\Resource\Index\Indexer
*/
protected function getIndexer(\TYPO3\CMS\Core\Resource\ResourceStorage $storage)
{
return \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Core\Resource\Index\Indexer::class, $storage);
}
}
@@ -0,0 +1,109 @@
<?php
namespace TYPO3\CMS\Scheduler\Task;
/*
* 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!
*/
/**
* Additional BE fields for tasks which indexes files in a storage
* @internal This class is a specific scheduler task implementation is not considered part of the Public TYPO3 API.
*/
class FileStorageIndexingAdditionalFieldProvider implements \TYPO3\CMS\Scheduler\AdditionalFieldProviderInterface
{
/**
* Add additional fields
*
* @param array $taskInfo Reference to the array containing the info used in the add/edit form
* @param AbstractTask|null $task When editing, reference to the current task. NULL when adding.
* @param \TYPO3\CMS\Scheduler\Controller\SchedulerModuleController $parentObject Reference to the calling object (Scheduler's BE module)
* @return array Array containing all the information pertaining to the additional fields
* @throws \InvalidArgumentException
*/
public function getAdditionalFields(array &$taskInfo, $task, \TYPO3\CMS\Scheduler\Controller\SchedulerModuleController $parentObject)
{
if ($task !== null && !$task instanceof FileStorageIndexingTask) {
throw new \InvalidArgumentException('Task not of type FileStorageExtractionTask', 1384275696);
}
$additionalFields['scheduler_fileStorageIndexing_storage'] = $this->getAllStoragesField($task, $taskInfo);
return $additionalFields;
}
/**
* Add a select field of available storages.
*
* @param FileStorageIndexingTask $task When editing, reference to the current task object. NULL when adding.
* @param array $taskInfo
* @return array Array containing all the information pertaining to the additional fields
*/
protected function getAllStoragesField(FileStorageIndexingTask $task = null, $taskInfo)
{
/** @var \TYPO3\CMS\Core\Resource\ResourceStorage[] $storages */
$storages = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Core\Resource\StorageRepository::class)->findAll();
$options = [];
foreach ($storages as $storage) {
$selected = '';
if ($task !== null && $task->storageUid === $storage->getUid()) {
$selected = ' selected="selected"';
} elseif ((int)$taskInfo['scheduler_fileStorageIndexing_storage'] === $storage->getUid()) {
$selected = ' selected="selected"';
}
$options[] = '<option value="' . $storage->getUid() . '" ' . $selected . ' >' . $storage->getName() . '</option>';
}
$fieldName = 'tx_scheduler[scheduler_fileStorageIndexing_storage]';
$fieldId = 'scheduler_fileStorageIndexing_storage';
$fieldHtml = '<select class="form-control" name="' . $fieldName . '" id="' . $fieldId . '">' . implode("\n", $options) . '</select>';
$fieldConfiguration = [
'code' => $fieldHtml,
'label' => 'LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:label.fileStorageIndexing.storage',
'cshKey' => '_MOD_system_txschedulerM1',
'cshLabel' => $fieldId
];
return $fieldConfiguration;
}
/**
* Validate additional fields
*
* @param array $submittedData Reference to the array containing the data submitted by the user
* @param \TYPO3\CMS\Scheduler\Controller\SchedulerModuleController $parentObject Reference to the calling object (Scheduler's BE module)
* @return bool True if validation was ok (or selected class is not relevant), false otherwise
*/
public function validateAdditionalFields(array &$submittedData, \TYPO3\CMS\Scheduler\Controller\SchedulerModuleController $parentObject)
{
$value = $submittedData['scheduler_fileStorageIndexing_storage'];
if (!\TYPO3\CMS\Core\Utility\MathUtility::canBeInterpretedAsInteger($value)) {
return false;
}
if (\TYPO3\CMS\Core\Resource\ResourceFactory::getInstance()->getStorageObject($submittedData['scheduler_fileStorageIndexing_storage']) !== null) {
return true;
}
return false;
}
/**
* Save additional field in task
*
* @param array $submittedData Contains data submitted by the user
* @param \TYPO3\CMS\Scheduler\Task\AbstractTask $task Reference to the current task object
* @throws \InvalidArgumentException
*/
public function saveAdditionalFields(array $submittedData, \TYPO3\CMS\Scheduler\Task\AbstractTask $task)
{
if (!$task instanceof FileStorageIndexingTask) {
throw new \InvalidArgumentException('Task not of type FileStorageExtractionTask', 1384275697);
}
$task->storageUid = (int)$submittedData['scheduler_fileStorageIndexing_storage'];
}
}
@@ -0,0 +1,58 @@
<?php
namespace TYPO3\CMS\Scheduler\Task;
/*
* 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!
*/
/**
* 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 \TYPO3\CMS\Scheduler\Task\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 = \TYPO3\CMS\Core\Resource\ResourceFactory::getInstance()->getStorageObject($this->storageUid);
$currentEvaluatePermissionsValue = $storage->getEvaluatePermissions();
$storage->setEvaluatePermissions(false);
$indexer = $this->getIndexer($storage);
$indexer->processChangesInStorages();
$storage->setEvaluatePermissions($currentEvaluatePermissionsValue);
}
return true;
}
/**
* Gets the indexer
*
* @param \TYPO3\CMS\Core\Resource\ResourceStorage $storage
* @return \TYPO3\CMS\Core\Resource\Index\Indexer
*/
protected function getIndexer(\TYPO3\CMS\Core\Resource\ResourceStorage $storage)
{
return \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Core\Resource\Index\Indexer::class, $storage);
}
}
@@ -0,0 +1,226 @@
<?php
namespace TYPO3\CMS\Scheduler\Task;
/*
* 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!
*/
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Messaging\FlashMessage;
use TYPO3\CMS\Scheduler\AbstractAdditionalFieldProvider;
use TYPO3\CMS\Scheduler\Controller\SchedulerModuleController;
use TYPO3\CMS\Scheduler\Task\Enumeration\Action;
/**
* Additional BE fields for ip address anonymization task.
* @internal This class is a specific scheduler task implementation is not considered part of the Public TYPO3 API.
*/
class IpAnonymizationAdditionalFieldProvider extends AbstractAdditionalFieldProvider
{
/**
* Add additional fields
*
* @param array $taskInfo Reference to the array containing the info used in the add/edit form
* @param AbstractTask|null $task When editing, reference to the current task. NULL when adding.
* @param SchedulerModuleController $schedulerModule Reference to the calling object (Scheduler's BE module)
* @return array Array containing all the information pertaining to the additional fields
*/
public function getAdditionalFields(array &$taskInfo, $task, SchedulerModuleController $schedulerModule)
{
$additionalFields = [];
$additionalFields['task_ipAnonymization_table'] = $this->getTableAdditionalField($taskInfo, $task, $schedulerModule);
$additionalFields['task_ipAnonymization_numberOfDays'] = $this->getNumberOfDaysAdditionalField($taskInfo, $task, $schedulerModule);
$additionalFields['task_ipAnonymization_mask'] = $this->getMaskAdditionalField($taskInfo, $task, $schedulerModule);
return $additionalFields;
}
/**
* Add a select field of available tables.
*
* @param array $taskInfo Reference to the array containing the info used in the add/edit form
* @param AbstractTask|null $task When editing, reference to the current task. NULL when adding.
* @param SchedulerModuleController $schedulerModule Reference to the calling object (Scheduler's BE module)
* @return array Array containing all the information pertaining to the additional fields
*/
protected function getTableAdditionalField(array &$taskInfo, $task, SchedulerModuleController $schedulerModule)
{
$currentSchedulerModuleAction = $schedulerModule->getCurrentAction();
$tableConfiguration = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][IpAnonymizationTask::class]['options']['tables'];
$options = [];
// Add an empty option on top if an existing task is configured
// with a table that can not be found in configuration anymore
if (!array_key_exists($task->table, $tableConfiguration) && $currentSchedulerModuleAction->equals(Action::EDIT)) {
$options[] = '<option value="" selected="selected"></option>';
}
foreach ($tableConfiguration as $tableName => $configuration) {
if ($currentSchedulerModuleAction->equals(Action::ADD) && empty($options)) {
// Select first table by default if adding a new task
$options[] = '<option value="' . $tableName . '" selected="selected">' . $tableName . '</option>';
} elseif ($task->table === $tableName) {
// Select currently selected table
$options[] = '<option value="' . $tableName . '" selected="selected">' . $tableName . '</option>';
} else {
$options[] = '<option value="' . $tableName . '">' . $tableName . '</option>';
}
}
$fieldName = 'tx_scheduler[scheduler_ipAnonymization_table]';
$fieldId = 'task_ipAnonymization_table';
$fieldHtml = [];
// Add table drop down html
$fieldHtml[] = '<select class="form-control" name="' . $fieldName . '" id="' . $fieldId . '">' . implode(LF, $options) . '</select>';
$fieldConfiguration = [
'code' => implode(LF, $fieldHtml),
'label' => 'LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:label.ipAnonymization.table',
'cshKey' => '_MOD_system_txschedulerM1',
'cshLabel' => $fieldId
];
return $fieldConfiguration;
}
/**
* Add an input field to get the number of days.
*
* @param array $taskInfo Reference to the array containing the info used in the add/edit form
* @param AbstractTask|null $task When editing, reference to the current task. NULL when adding.
* @param SchedulerModuleController $schedulerModule Reference to the calling object (Scheduler's BE module)
* @return array Array containing all the information pertaining to the additional fields
*/
protected function getNumberOfDaysAdditionalField(array &$taskInfo, $task, SchedulerModuleController $schedulerModule)
{
$fieldId = 'scheduler_ipAnonymization_numberOfDays';
if (empty($taskInfo[$fieldId])) {
$taskInfo[$fieldId] = $task->numberOfDays ?? 180;
}
$fieldName = 'tx_scheduler[' . $fieldId . ']';
$fieldHtml = '<input class="form-control" type="text" ' . 'name="' . $fieldName . '" ' . 'id="' . $fieldId . '" ' . 'value="' . (int)$taskInfo[$fieldId] . '" ' . 'size="4">';
$fieldConfiguration = [
'code' => $fieldHtml,
'label' => 'LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:label.ipAnonymization.numberOfDays',
'cshKey' => '_MOD_system_txschedulerM1',
'cshLabel' => $fieldId
];
return $fieldConfiguration;
}
/**
* Add an input field to get the mask.
*
* @param array $taskInfo Reference to the array containing the info used in the add/edit form
* @param AbstractTask|null $task When editing, reference to the current task. NULL when adding.
* @param SchedulerModuleController $schedulerModule Reference to the calling object (Scheduler's BE module)
* @return array Array containing all the information pertaining to the additional fields
*/
protected function getMaskAdditionalField(array &$taskInfo, $task, SchedulerModuleController $schedulerModule)
{
$fieldId = 'scheduler_ipAnonymization_mask';
if (empty($taskInfo[$fieldId])) {
$taskInfo[$fieldId] = $task->mask ?? 2;
}
$fieldName = 'tx_scheduler[' . $fieldId . ']';
$fieldHtml = '';
foreach ([1, 2] as $mask) {
$selected = (int)$taskInfo[$fieldId] === $mask ? ' selected' : '';
$fieldHtml .= '<option value="' . $mask . '"' . $selected . '>'
. htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:label.ipAnonymization.mask.' . $mask))
. '</option>';
}
$fieldHtml = '<select class="form-control" name="' . $fieldName . '" ' . 'id="' . $fieldId . '">' . $fieldHtml . '</select>';
$fieldConfiguration = [
'code' => $fieldHtml,
'label' => 'LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:label.ipAnonymization.mask',
'cshKey' => '_MOD_system_txschedulerM1',
'cshLabel' => $fieldId
];
return $fieldConfiguration;
}
/**
* Validate additional fields
*
* @param array $submittedData Reference to the array containing the data submitted by the user
* @param SchedulerModuleController $schedulerModule Reference to the calling object (Scheduler's BE module)
* @return bool True if validation was ok (or selected class is not relevant), false otherwise
*/
public function validateAdditionalFields(array &$submittedData, SchedulerModuleController $schedulerModule)
{
$validData = $this->validateTableAdditionalField($submittedData, $schedulerModule);
$validData &= $this->validateNumberOfDaysAdditionalField($submittedData, $schedulerModule);
return $validData;
}
/**
* Checks given table for existence in configuration array
*
* @param array $submittedData Reference to the array containing the data submitted by the user
* @param SchedulerModuleController $schedulerModule Reference to the calling object (Scheduler's BE module)
* @return bool True if table exists in configuration, false otherwise
*/
public function validateTableAdditionalField(array &$submittedData, SchedulerModuleController $schedulerModule)
{
$validData = false;
$tableConfiguration = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][IpAnonymizationTask::class]['options']['tables'];
if (!isset($submittedData['scheduler_ipAnonymization_table'])) {
$validData = true;
} elseif (array_key_exists($submittedData['scheduler_ipAnonymization_table'], $tableConfiguration)) {
$validData = true;
}
return $validData;
}
/**
* Checks if given number of days is a positive integer
*
* @param array $submittedData Reference to the array containing the data submitted by the user
* @param SchedulerModuleController $schedulerModule Reference to the calling object (Scheduler's BE module)
* @return bool True if validation was ok (or selected class is not relevant), false otherwise
*/
public function validateNumberOfDaysAdditionalField(array &$submittedData, SchedulerModuleController $schedulerModule)
{
$validData = false;
if (!isset($submittedData['scheduler_ipAnonymization_numberOfDays'])) {
$validData = true;
} elseif ((int)$submittedData['scheduler_ipAnonymization_numberOfDays'] >= 0) {
$validData = true;
} else {
// Issue error message
$this->addMessage($this->getLanguageService()->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:msg.invalidNumberOfDays'), FlashMessage::ERROR);
}
return $validData;
}
/**
* Save additional field in task
*
* @param array $submittedData Contains data submitted by the user
* @param \TYPO3\CMS\Scheduler\Task\AbstractTask $task Reference to the current task object
*/
public function saveAdditionalFields(array $submittedData, AbstractTask $task)
{
$task->table = $submittedData['scheduler_ipAnonymization_table'];
$task->mask = (int)$submittedData['scheduler_ipAnonymization_mask'];
$task->numberOfDays = (int)$submittedData['scheduler_ipAnonymization_numberOfDays'];
}
/**
* Returns an instance of LanguageService
*
* @return LanguageService
*/
protected function getLanguageService(): LanguageService
{
return $GLOBALS['LANG'];
}
}
@@ -0,0 +1,148 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Scheduler\Task;
/*
* 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!
*/
use TYPO3\CMS\Core\Database\ConnectionPool;
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['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][\TYPO3\CMS\Scheduler\Task\IpAnonymizationTask::class]['options']['tables']
* See ext_localconf.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 = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][self::class]['options']['tables'][$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($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, \PDO::PARAM_INT)
),
$queryBuilder->expr()->neq(
$configuration['ipField'],
$queryBuilder->createNamedParameter('', \PDO::PARAM_STR)
),
$queryBuilder->expr()->isNotNull($configuration['ipField']),
$queryBuilder->expr()->notLike(
$configuration['ipField'],
$queryBuilder->createNamedParameter($notLikeMaskPattern, \PDO::PARAM_STR)
),
$queryBuilder->expr()->notLike(
$configuration['ipField'],
$queryBuilder->createNamedParameter('%::', \PDO::PARAM_STR)
)
)
->from($table)
->execute();
while ($row = $result->fetch()) {
$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;
}
/**
* This method returns the selected table as additional information
*
* @return string Information to display
*/
public function getAdditionalInformation()
{
return sprintf($GLOBALS['LANG']->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:label.ipAnonymization.additionalInformationTable'), $this->table, $this->numberOfDays);
}
}
@@ -0,0 +1,231 @@
<?php
namespace TYPO3\CMS\Scheduler\Task;
/*
* 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!
*/
use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Messaging\FlashMessage;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Scheduler\AbstractAdditionalFieldProvider;
use TYPO3\CMS\Scheduler\Controller\SchedulerModuleController;
use TYPO3\CMS\Scheduler\Task\Enumeration\Action;
/**
* Additional BE fields for optimize database table task.
* @internal This class is a specific scheduler task implementation is not considered part of the Public TYPO3 API.
*/
class OptimizeDatabaseTableAdditionalFieldProvider extends AbstractAdditionalFieldProvider
{
/**
* @var string
*/
protected $languageFile = 'LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf';
/**
* Add a multi select box with all available database tables.
*
* @param array $taskInfo Reference to the array containing the info used in the add/edit form
* @param AbstractTask|null $task When editing, reference to the current task. NULL when adding.
* @param SchedulerModuleController $schedulerModule Reference to the calling object (Scheduler's BE module)
* @return array Array containing all the information pertaining to the additional fields
*/
public function getAdditionalFields(array &$taskInfo, $task, SchedulerModuleController $schedulerModule)
{
$currentSchedulerModuleAction = $schedulerModule->getCurrentAction();
// Initialize selected fields
if (empty($taskInfo['scheduler_optimizeDatabaseTables_selectedTables'])) {
$taskInfo['scheduler_optimizeDatabaseTables_selectedTables'] = [];
if ($currentSchedulerModuleAction->equals(Action::ADD)) {
// In case of new task, select no tables by default
$taskInfo['scheduler_optimizeDatabaseTables_selectedTables'] = [];
} elseif ($currentSchedulerModuleAction->equals(Action::EDIT)) {
// In case of editing the task, set to currently selected value
$taskInfo['scheduler_optimizeDatabaseTables_selectedTables'] = $task->selectedTables;
}
}
$fieldName = 'tx_scheduler[scheduler_optimizeDatabaseTables_selectedTables][]';
$fieldId = 'scheduler_optimizeDatabaseTables_selectedTables';
$fieldOptions = $this->getDatabaseTableOptions($taskInfo['scheduler_optimizeDatabaseTables_selectedTables']);
$fieldHtml = '<select class="form-control" name="' . $fieldName
. '" id="' . $fieldId
. '" class="from-control" size="10" multiple="multiple">'
. $fieldOptions
. '</select>';
$additionalFields[$fieldId] = [
'code' => $fieldHtml,
'label' => $this->languageFile . ':label.optimizeDatabaseTables.selectTables',
'cshKey' => '_MOD_system_txschedulerM1',
'cshLabel' => $fieldId,
];
return $additionalFields;
}
/**
* Checks that all selected backends exist in available backend list
*
* @param array $submittedData Reference to the array containing the data submitted by the user
* @param SchedulerModuleController $schedulerModule Reference to the calling object (Scheduler's BE module)
* @return bool TRUE if validation was ok (or selected class is not relevant), FALSE otherwise
*/
public function validateAdditionalFields(array &$submittedData, SchedulerModuleController $schedulerModule)
{
$validData = true;
$availableTables = $this->getOptimizableTables();
if (is_array($submittedData['scheduler_optimizeDatabaseTables_selectedTables'])) {
$invalidTables = array_diff(
$submittedData['scheduler_optimizeDatabaseTables_selectedTables'],
$availableTables
);
if (!empty($invalidTables)) {
$this->addMessage(
$GLOBALS['LANG']->sL($this->languageFile . ':msg.selectionOfNonExistingDatabaseTables'),
FlashMessage::ERROR
);
$validData = false;
}
} else {
$this->addMessage(
$GLOBALS['LANG']->sL($this->languageFile . ':msg.noDatabaseTablesSelected'),
FlashMessage::ERROR
);
$validData = false;
}
return $validData;
}
/**
* Save selected backends in task object
*
* @param array $submittedData Contains data submitted by the user
* @param AbstractTask $task Reference to the current task object
*/
public function saveAdditionalFields(array $submittedData, AbstractTask $task)
{
$task->selectedTables = $submittedData['scheduler_optimizeDatabaseTables_selectedTables'];
}
/**
* Build select options of available backends and set currently selected backends
*
* @param array $selectedTables Selected backends
* @return string HTML of selectbox options
*/
protected function getDatabaseTableOptions(array $selectedTables)
{
$options = [];
$availableTables = $this->getOptimizableTables();
foreach ($availableTables as $tableName) {
$selected = in_array($tableName, $selectedTables, true) ? ' selected="selected"' : '';
$options[] = '<option value="' . $tableName . '"' . $selected . '>' . $tableName . '</option>';
}
return implode('', $options);
}
/**
* Get all tables that are capable of optimization
*
* @return array Names of table that can be optimized.
*/
protected function getOptimizableTables()
{
$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,
function ($value) use ($connectionName) {
return $value === $connectionName;
}
));
$tables = $this->getOptimizableTablesForConnection($connection, $tablesOnConnection);
$optimizableTables = array_merge($optimizableTables, $tables);
}
}
sort($optimizableTables);
return $optimizableTables;
}
/**
* Retrieve all optimizable tables for a connection, optionally restricted to the subset
* of table names in the $tableNames array.
*
* @param Connection $connection
* @param array $tableNames
* @return array
*/
protected function getOptimizableTablesForConnection(Connection $connection, array $tableNames = []): array
{
// Return empty list if the database platform is not MySQL
if (strpos($connection->getServerVersion(), 'MySQL') !== 0) {
return [];
}
// Retrieve all tables from the MySQL informaation 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', \PDO::PARAM_STR)
),
$queryBuilder->expr()->in(
'ENGINE',
$queryBuilder->createNamedParameter(['InnoDB', 'MyISAM', 'ARCHIVE'], Connection::PARAM_STR_ARRAY)
),
$queryBuilder->expr()->eq(
'TABLE_SCHEMA',
$queryBuilder->createNamedParameter($connection->getDatabase(), \PDO::PARAM_STR)
)
);
if (!empty($tableNames)) {
$queryBuilder->andWhere(
$queryBuilder->expr()->in(
'TABLE_NAME',
$queryBuilder->createNamedParameter($tableNames, \PDO::PARAM_STR)
)
);
}
$tables = $queryBuilder->execute()->fetchAll();
return array_column($tables, 'Table');
}
}
@@ -0,0 +1,74 @@
<?php
namespace TYPO3\CMS\Scheduler\Task;
/*
* 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!
*/
use Doctrine\DBAL\DBALException;
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);
if (strpos($connection->getServerVersion(), 'MySQL') === 0) {
try {
$connection->query('OPTIMIZE TABLE ' . $connection->quoteIdentifier($tableName));
} catch (DBALException $e) {
throw new \RuntimeException(
TableGarbageCollectionTask::class . ' failed for: ' . $tableName . ': ' .
$e->getPrevious()->getMessage(),
1441390263
);
}
}
}
return true;
}
/**
* Output the selected tables
*
* @return string
*/
public function getAdditionalInformation()
{
return implode(', ', $this->selectedTables);
}
}
@@ -0,0 +1,100 @@
<?php
namespace TYPO3\CMS\Scheduler\Task;
/*
* 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!
*/
use TYPO3\CMS\Core\Messaging\FlashMessage;
use TYPO3\CMS\Scheduler\AbstractAdditionalFieldProvider;
use TYPO3\CMS\Scheduler\Controller\SchedulerModuleController;
use TYPO3\CMS\Scheduler\Task\Enumeration\Action;
/**
* Additional BE fields for recycler garbage collection task.
*
* Creates an integer input field for difference between scheduler run time
* and file modification time in days to select from.
* @internal This class is a specific scheduler task implementation is not considered part of the Public TYPO3 API.
*/
class RecyclerGarbageCollectionAdditionalFieldProvider extends AbstractAdditionalFieldProvider
{
/**
* Default period in days to remove a recycled file
*
* @var int Default number of days
*/
protected $defaultNumberOfDays = 30;
/**
* Add an integer input field for difference between scheduler run time
* and file modification time in days to select from
*
* @param array $taskInfo Reference to the array containing the info used in the add/edit form
* @param AbstractTask|null $task When editing, reference to the current task. NULL when adding.
* @param SchedulerModuleController $schedulerModule Reference to the calling object (Scheduler's BE module)
* @return array Array containing all the information pertaining to the additional fields
*/
public function getAdditionalFields(array &$taskInfo, $task, SchedulerModuleController $schedulerModule)
{
$currentSchedulerModuleAction = $schedulerModule->getCurrentAction();
// Initialize selected fields
if (!isset($taskInfo['scheduler_recyclerGarbageCollection_numberOfDays'])) {
$taskInfo['scheduler_recyclerGarbageCollection_numberOfDays'] = $this->defaultNumberOfDays;
if ($currentSchedulerModuleAction->equals(Action::EDIT)) {
$taskInfo['scheduler_recyclerGarbageCollection_numberOfDays'] = $task->numberOfDays;
}
}
$fieldName = 'tx_scheduler[scheduler_recyclerGarbageCollection_numberOfDays]';
$fieldId = 'task_recyclerGarbageCollection_numberOfDays';
$fieldValue = (int)$taskInfo['scheduler_recyclerGarbageCollection_numberOfDays'];
$fieldHtml = '<input class="form-control" type="text" name="' . $fieldName . '" id="' . $fieldId . '" value="' . htmlspecialchars($fieldValue) . '">';
$additionalFields[$fieldId] = [
'code' => $fieldHtml,
'label' => 'LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:label.recyclerGarbageCollection.numberOfDays',
'cshKey' => '_MOD_system_txschedulerM1',
'cshLabel' => $fieldId
];
return $additionalFields;
}
/**
* Checks if the given value is an integer
*
* @param array $submittedData Reference to the array containing the data submitted by the user
* @param SchedulerModuleController $schedulerModule Reference to the calling object (Scheduler's BE module)
* @return bool TRUE if validation was ok (or selected class is not relevant), FALSE otherwise
*/
public function validateAdditionalFields(array &$submittedData, SchedulerModuleController $schedulerModule)
{
$result = true;
// Check if number of days is indeed a number and greater or equals to 0
// If not, fail validation and issue error message
if (!is_numeric($submittedData['scheduler_recyclerGarbageCollection_numberOfDays']) || (int)$submittedData['scheduler_recyclerGarbageCollection_numberOfDays'] < 0) {
$result = false;
$this->addMessage($GLOBALS['LANG']->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:msg.invalidNumberOfDays'), FlashMessage::ERROR);
}
return $result;
}
/**
* Saves given integer value in task object
*
* @param array $submittedData Contains data submitted by the user
* @param AbstractTask $task Reference to the current task object
*/
public function saveAdditionalFields(array $submittedData, AbstractTask $task)
{
$task->numberOfDays = (int)$submittedData['scheduler_recyclerGarbageCollection_numberOfDays'];
}
}
@@ -0,0 +1,94 @@
<?php
namespace TYPO3\CMS\Scheduler\Task;
/*
* 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!
*/
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 \TYPO3\CMS\Scheduler\Task\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);
}
}
}
}
@@ -0,0 +1,265 @@
<?php
namespace TYPO3\CMS\Scheduler\Task;
/*
* 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!
*/
use TYPO3\CMS\Core\Messaging\FlashMessage;
use TYPO3\CMS\Scheduler\AbstractAdditionalFieldProvider;
use TYPO3\CMS\Scheduler\Controller\SchedulerModuleController;
use TYPO3\CMS\Scheduler\Task\Enumeration\Action;
/**
* Additional BE fields for sys log table garbage collection task.
* @internal This class is a specific scheduler task implementation is not considered part of the Public TYPO3 API.
*/
class TableGarbageCollectionAdditionalFieldProvider extends AbstractAdditionalFieldProvider
{
/**
* @var array Default number of days by table
*/
protected $defaultNumberOfDays = [];
/**
* Add additional fields
*
* @param array $taskInfo Reference to the array containing the info used in the add/edit form
* @param AbstractTask|null $task When editing, reference to the current task. NULL when adding.
* @param SchedulerModuleController $schedulerModule Reference to the calling object (Scheduler's BE module)
* @return array Array containing all the information pertaining to the additional fields
*/
public function getAdditionalFields(array &$taskInfo, $task, SchedulerModuleController $schedulerModule)
{
$this->initDefaultNumberOfDays();
$additionalFields = [];
$additionalFields['task_tableGarbageCollection_allTables'] = $this->getAllTablesAdditionalField($taskInfo, $task, $schedulerModule);
$additionalFields['task_tableGarbageCollection_table'] = $this->getTableAdditionalField($taskInfo, $task, $schedulerModule);
$additionalFields['task_tableGarbageCollection_numberOfDays'] = $this->getNumberOfDaysAdditionalField($taskInfo, $task, $schedulerModule);
return $additionalFields;
}
/**
* Initialize the default number of days for all configured tables
*/
protected function initDefaultNumberOfDays()
{
$tableConfiguration = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][TableGarbageCollectionTask::class]['options']['tables'];
foreach ($tableConfiguration as $tableName => $configuration) {
if (isset($configuration['expirePeriod'])) {
$this->defaultNumberOfDays[$tableName] = $configuration['expirePeriod'];
}
}
}
/**
* Add a select field of available tables.
*
* @param array $taskInfo Reference to the array containing the info used in the add/edit form
* @param AbstractTask|null $task When editing, reference to the current task. NULL when adding.
* @param SchedulerModuleController $schedulerModule Reference to the calling object (Scheduler's BE module)
* @return array Array containing all the information pertaining to the additional fields
*/
protected function getAllTablesAdditionalField(array &$taskInfo, $task, SchedulerModuleController $schedulerModule)
{
$currentSchedulerModuleAction = $schedulerModule->getCurrentAction();
if ($currentSchedulerModuleAction->equals(Action::EDIT)) {
$checked = $task->allTables === true ? 'checked="checked" ' : '';
} else {
$checked = '';
}
$fieldName = 'tx_scheduler[scheduler_tableGarbageCollection_allTables]';
$fieldId = 'task_tableGarbageCollection_allTables';
$fieldHtml = '<div class="checkbox"><label><input type="checkbox" ' . $checked . ' name="' . $fieldName . '" ' . 'id="' . $fieldId . '"></label></div>';
$fieldConfiguration = [
'code' => $fieldHtml,
'label' => 'LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:label.tableGarbageCollection.allTables',
'cshKey' => '_MOD_system_txschedulerM1',
'cshLabel' => $fieldId
];
return $fieldConfiguration;
}
/**
* Add a select field of available tables.
*
* @param array $taskInfo Reference to the array containing the info used in the add/edit form
* @param AbstractTask|null $task When editing, reference to the current task. NULL when adding.
* @param SchedulerModuleController $schedulerModule Reference to the calling object (Scheduler's BE module)
* @return array Array containing all the information pertaining to the additional fields
*/
protected function getTableAdditionalField(array &$taskInfo, $task, SchedulerModuleController $schedulerModule)
{
$currentSchedulerModuleAction = $schedulerModule->getCurrentAction();
$tableConfiguration = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][TableGarbageCollectionTask::class]['options']['tables'];
$options = [];
// Add an empty option on top if an existing task is configured
// with a table that can not be found in configuration anymore
if (!array_key_exists($task->table, $tableConfiguration) && $currentSchedulerModuleAction->equals(Action::EDIT)) {
$options[] = '<option value="" selected="selected"></option>';
}
foreach ($tableConfiguration as $tableName => $configuration) {
if ($currentSchedulerModuleAction->equals(Action::ADD) && empty($options)) {
// Select first table by default if adding a new task
$options[] = '<option value="' . $tableName . '" selected="selected">' . $tableName . '</option>';
} elseif ($task->table === $tableName) {
// Select currently selected table
$options[] = '<option value="' . $tableName . '" selected="selected">' . $tableName . '</option>';
} else {
$options[] = '<option value="' . $tableName . '">' . $tableName . '</option>';
}
}
$disabled = $task->allTables === true ? ' disabled="disabled"' : '';
$fieldName = 'tx_scheduler[scheduler_tableGarbageCollection_table]';
$fieldId = 'task_tableGarbageCollection_table';
$fieldHtml = [];
// Add table drop down html
$fieldHtml[] = '<select class="form-control" name="' . $fieldName . '" ' . $disabled . ' id="' . $fieldId . '">' . implode(LF, $options) . '</select>';
// Add js array for default 'number of days' values
$fieldHtml[] = '<script type="text/javascript">/*<![CDATA[*/<!--';
$fieldHtml[] = 'var defaultNumberOfDays = ' . json_encode($this->defaultNumberOfDays) . ';';
$fieldHtml[] = '// -->/*]]>*/</script>';
$fieldConfiguration = [
'code' => implode(LF, $fieldHtml),
'label' => 'LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:label.tableGarbageCollection.table',
'cshKey' => '_MOD_system_txschedulerM1',
'cshLabel' => $fieldId
];
return $fieldConfiguration;
}
/**
* Add an input field to get the number of days.
*
* @param array $taskInfo Reference to the array containing the info used in the add/edit form
* @param AbstractTask|null $task When editing, reference to the current task. NULL when adding.
* @param SchedulerModuleController $schedulerModule Reference to the calling object (Scheduler's BE module)
* @return array Array containing all the information pertaining to the additional fields
*/
protected function getNumberOfDaysAdditionalField(array &$taskInfo, $task, SchedulerModuleController $schedulerModule)
{
$currentSchedulerModuleAction = $schedulerModule->getCurrentAction();
// Initialize selected fields
$disabled = '';
if (empty($taskInfo['scheduler_tableGarbageCollection_numberOfDays'])) {
if ($currentSchedulerModuleAction->equals(Action::ADD)) {
// In case of new task, set to 180 days
$taskInfo['scheduler_tableGarbageCollection_numberOfDays'] = 180;
} elseif ($currentSchedulerModuleAction->equals(Action::EDIT)) {
// In case of editing the task, set to currently selected value
$taskInfo['scheduler_tableGarbageCollection_numberOfDays'] = $task->numberOfDays;
if ($task->numberOfDays === 0 && !isset($this->defaultNumberOfDays[$task->table])) {
$disabled = ' disabled="disabled"';
}
}
}
if ($task->allTables === true) {
$disabled = ' disabled="disabled"';
}
$fieldName = 'tx_scheduler[scheduler_tableGarbageCollection_numberOfDays]';
$fieldId = 'task_tableGarbageCollection_numberOfDays';
$fieldHtml = '<input class="form-control" type="text" ' . 'name="' . $fieldName . '" ' . 'id="' . $fieldId . '" ' . $disabled . 'value="' . (int)$taskInfo['scheduler_tableGarbageCollection_numberOfDays'] . '" ' . 'size="4">';
$fieldConfiguration = [
'code' => $fieldHtml,
'label' => 'LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:label.tableGarbageCollection.numberOfDays',
'cshKey' => '_MOD_system_txschedulerM1',
'cshLabel' => $fieldId
];
return $fieldConfiguration;
}
/**
* Validate additional fields
*
* @param array $submittedData Reference to the array containing the data submitted by the user
* @param SchedulerModuleController $schedulerModule Reference to the calling object (Scheduler's BE module)
* @return bool True if validation was ok (or selected class is not relevant), false otherwise
*/
public function validateAdditionalFields(array &$submittedData, SchedulerModuleController $schedulerModule)
{
$validData = $this->validateAllTablesAdditionalField($submittedData);
$validData &= $this->validateTableAdditionalField($submittedData);
$validData &= $this->validateNumberOfDaysAdditionalField($submittedData);
return $validData;
}
/**
* Checks if all table field is correct
*
* @param array $submittedData Reference to the array containing the data submitted by the user
* @return bool True if data is valid
*/
public function validateAllTablesAdditionalField(array &$submittedData)
{
$validData = false;
if (!isset($submittedData['scheduler_tableGarbageCollection_allTables'])) {
$validData = true;
} elseif ($submittedData['scheduler_tableGarbageCollection_allTables'] === 'on') {
$validData = true;
}
return $validData;
}
/**
* Checks given table for existence in configuration array
*
* @param array $submittedData Reference to the array containing the data submitted by the user
* @return bool True if table exists in configuration, false otherwise
*/
public function validateTableAdditionalField(array &$submittedData)
{
$validData = false;
$tableConfiguration = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][TableGarbageCollectionTask::class]['options']['tables'];
if (!isset($submittedData['scheduler_tableGarbageCollection_table'])) {
$validData = true;
} elseif (array_key_exists($submittedData['scheduler_tableGarbageCollection_table'], $tableConfiguration)) {
$validData = true;
}
return $validData;
}
/**
* Checks if given number of days is a positive integer
*
* @param array $submittedData Reference to the array containing the data submitted by the user
* @return bool True if validation was ok (or selected class is not relevant), false otherwise
*/
public function validateNumberOfDaysAdditionalField(array &$submittedData)
{
$validData = false;
if (!isset($submittedData['scheduler_tableGarbageCollection_numberOfDays'])) {
$validData = true;
} elseif ((int)$submittedData['scheduler_tableGarbageCollection_numberOfDays'] >= 0) {
$validData = true;
} else {
// Issue error message
$this->addMessage($GLOBALS['LANG']->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:msg.invalidNumberOfDays'), FlashMessage::ERROR);
}
return $validData;
}
/**
* Save additional field in task
*
* @param array $submittedData Contains data submitted by the user
* @param \TYPO3\CMS\Scheduler\Task\AbstractTask $task Reference to the current task object
*/
public function saveAdditionalFields(array $submittedData, AbstractTask $task)
{
$task->allTables = $submittedData['scheduler_tableGarbageCollection_allTables'] === 'on';
$task->table = $submittedData['scheduler_tableGarbageCollection_table'];
$task->numberOfDays = (int)$submittedData['scheduler_tableGarbageCollection_numberOfDays'];
}
}
@@ -0,0 +1,131 @@
<?php
namespace TYPO3\CMS\Scheduler\Task;
/*
* 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!
*/
use Doctrine\DBAL\DBALException;
use TYPO3\CMS\Core\Database\ConnectionPool;
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['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][\TYPO3\CMS\Scheduler\Task\TableGarbageCollectionTask::class]['options']['tables']
* See ext_localconf.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 = 180;
/**
* @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 = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][self::class]['options']['tables'];
$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($table, array $configuration)
{
$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, \PDO::PARAM_INT)),
$queryBuilder->expr()->gt($field, $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT))
);
} elseif (!empty($configuration['dateField'])) {
if (!$this->allTables) {
$deleteTimestamp = strtotime('-' . $this->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, \PDO::PARAM_INT)
)
);
} else {
throw new \RuntimeException(self::class . ' misconfiguration: Either expireField or dateField must be defined for table ' . $table, 1308355268);
}
try {
$queryBuilder->execute();
} 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 = $GLOBALS['LANG']->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:label.tableGarbageCollection.additionalInformationAllTables');
} else {
$message = sprintf($GLOBALS['LANG']->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:label.tableGarbageCollection.additionalInformationTable'), $this->table);
}
return $message;
}
}
@@ -0,0 +1,62 @@
<?php
namespace TYPO3\CMS\Scheduler\ViewHelpers;
/*
* 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!
*/
use TYPO3\CMS\Backend\Routing\UriBuilder;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
use TYPO3Fluid\Fluid\Core\ViewHelper\Traits\CompileWithRenderStatic;
/**
* Create internal link within backend app
* @internal
*/
class ModuleLinkViewHelper extends AbstractViewHelper
{
use CompileWithRenderStatic;
/**
* Initializes the arguments
*/
public function initializeArguments()
{
$this->registerArgument('controller', 'string', 'The "controller" of scheduler. Possible values are "scheduler", "check", "info"', true);
$this->registerArgument('action', 'string', 'The action to be called within each controller', true);
$this->registerArgument('arguments', 'array', '', false, []);
}
/**
* Render module link with command and arguments
*
* @param array $arguments
* @param \Closure $renderChildrenClosure
* @param RenderingContextInterface $renderingContext
*
* @return string
*/
public static function renderStatic(array $arguments, \Closure $renderChildrenClosure, RenderingContextInterface $renderingContext)
{
$moduleArguments = [];
$moduleArguments['SET']['function'] = $arguments['controller'];
$moduleArguments['CMD'] = $arguments['action'];
if (!empty($arguments['arguments'])) {
$moduleArguments['tx_scheduler'] = $arguments['arguments'];
}
$uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
return (string)$uriBuilder->buildUriFromRoute('system_txschedulerM1', $moduleArguments);
}
}