236 lines
11 KiB
PHP
236 lines
11 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
/*
|
|
* This file is part of the TYPO3 CMS project.
|
|
*
|
|
* It is free software; you can redistribute it and/or modify it under
|
|
* the terms of the GNU General Public License, either version 2
|
|
* of the License, or any later version.
|
|
*
|
|
* For the full copyright and license information, please read the
|
|
* LICENSE.txt file that was distributed with this source code.
|
|
*
|
|
* The TYPO3 project - inspiring people to share!
|
|
*/
|
|
|
|
namespace TYPO3\CMS\Scheduler\Migration;
|
|
|
|
use Doctrine\DBAL\ArrayParameterType;
|
|
use Doctrine\DBAL\ParameterType;
|
|
use TYPO3\CMS\Core\Attribute\UpgradeWizard;
|
|
use TYPO3\CMS\Core\Database\ConnectionPool;
|
|
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
|
|
use TYPO3\CMS\Core\Serializer\DenyListDeserializer;
|
|
use TYPO3\CMS\Core\Upgrades\DatabaseUpdatedPrerequisite;
|
|
use TYPO3\CMS\Core\Upgrades\UpgradeWizardInterface;
|
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
|
use TYPO3\CMS\Scheduler\Service\TaskService;
|
|
use TYPO3\CMS\Scheduler\Task\AbstractTask;
|
|
use TYPO3\CMS\Scheduler\Task\ExecuteSchedulableCommandTask;
|
|
use TYPO3\CMS\Scheduler\Task\TaskSerializer;
|
|
|
|
/**
|
|
* @since 14.0
|
|
* @internal This class is only meant to be used within EXT:scheduler and is not part of the TYPO3 Core API.
|
|
*/
|
|
#[UpgradeWizard('schedulerDatabaseStorageMigration')]
|
|
class SchedulerDatabaseStorageMigration implements UpgradeWizardInterface
|
|
{
|
|
protected const TABLE_NAME = 'tx_scheduler_task';
|
|
|
|
public function __construct(private readonly DenyListDeserializer $deserializer) {}
|
|
|
|
public function getTitle(): string
|
|
{
|
|
return 'Migrate the contents of the tx_scheduler_task database table into a more structured form.';
|
|
}
|
|
|
|
public function getDescription(): string
|
|
{
|
|
return 'Each Scheduler Task was previously stored in a serialized object format in the database. This update wizard migrates records of this type to a JSON-formatted storage in the database. If this wizard does not disappear, it means there are tasks that failed to be migrated and may need manual inspection or re-creation. When this happens, inspect all tasks of tx_scheduler_task where the "tasktype" column is empty.';
|
|
}
|
|
|
|
public function getPrerequisites(): array
|
|
{
|
|
return [
|
|
DatabaseUpdatedPrerequisite::class,
|
|
];
|
|
}
|
|
|
|
public function updateNecessary(): bool
|
|
{
|
|
return $this->hasRecordsToUpdate();
|
|
}
|
|
|
|
public function executeUpdate(): bool
|
|
{
|
|
$connection = $this->getConnectionPool()->getConnectionForTable(self::TABLE_NAME);
|
|
$table = $this->getConnectionPool()->getConnectionForTable(self::TABLE_NAME)->createSchemaManager()->introspectSchema()->getTable(self::TABLE_NAME);
|
|
$taskSerializer = GeneralUtility::makeInstance(TaskSerializer::class);
|
|
$taskService = GeneralUtility::makeInstance(TaskService::class);
|
|
$hasFailures = false;
|
|
foreach ($this->getRecordsToUpdate() as $record) {
|
|
try {
|
|
// Base migration was already done, but not the migration to additional fields, so we'll do this now
|
|
if (!empty($record['tasktype'])) {
|
|
$taskObject = $taskSerializer->deserialize($record);
|
|
} else {
|
|
// unserialize() will only give a E_NOTICE and false result, not throw an error. Silence this
|
|
// (for tests) and operate on the "false". If future PHP promotes this to an exception, the Throwable
|
|
// catch will kick in.
|
|
$taskObject = $this->deserializer->deserialize($record['serialized_task_object']);
|
|
}
|
|
if ($taskObject instanceof AbstractTask) {
|
|
$fieldsToUpdate = [
|
|
'tasktype' => $taskObject->getTaskType(),
|
|
'execution_details' => $taskObject->getExecution()?->toArray(),
|
|
];
|
|
$taskDetails = $taskService->getTaskDetailsFromTask($taskObject);
|
|
$taskParameters = $taskObject->getTaskParameters();
|
|
if (($taskDetails['isNativeTask'] ?? false) && $taskDetails['className'] !== ExecuteSchedulableCommandTask::class) {
|
|
// map native types to real fields, and do not use the parameters' value. Only
|
|
// exception to this are console commands, which are native types but use the
|
|
// parameters as well, because they have dynamic configuration (arguments, options).
|
|
if (is_array($taskDetails['additionalFields'] ?? false) && $taskDetails['additionalFields'] !== []) {
|
|
foreach ($taskDetails['additionalFields'] as $additionalFieldName) {
|
|
$fieldsToUpdate[$additionalFieldName] = $taskParameters[$additionalFieldName] ?? null;
|
|
}
|
|
}
|
|
$fieldsToUpdate['parameters'] = null;
|
|
} else {
|
|
$fieldsToUpdate['parameters'] = $taskParameters;
|
|
}
|
|
$connection->update(
|
|
self::TABLE_NAME,
|
|
array_filter($fieldsToUpdate, static fn($column) => $table->hasColumn($column), ARRAY_FILTER_USE_KEY),
|
|
['uid' => (int)$record['uid']]
|
|
);
|
|
} elseif ($taskObject instanceof \__PHP_Incomplete_Class) {
|
|
$objectVars = get_mangled_object_vars($taskObject);
|
|
$properties = [];
|
|
$executionDetails = null;
|
|
$taskType = null;
|
|
foreach ($objectVars as $key => $value) {
|
|
$key = trim($key);
|
|
$key = trim($key, "*\0");
|
|
$key = trim($key);
|
|
if ($key === '__PHP_Incomplete_Class_Name') {
|
|
$taskType = $value;
|
|
} else {
|
|
switch ($key) {
|
|
case '__PHP_Incomplete_Class_Name':
|
|
$taskType = $value;
|
|
break;
|
|
case 'execution':
|
|
$executionDetails = $value;
|
|
break;
|
|
case 'progress':
|
|
case 'scheduler':
|
|
case 'taskUid':
|
|
case 'disabled':
|
|
case 'runOnNextCronJob': // mapped to "task_group" in the database
|
|
case 'executionTime': // mapped to "next_execution" in the database
|
|
case 'taskGroup': // mapped to "task_group" in the database
|
|
case 'description':
|
|
break;
|
|
default:
|
|
if (is_scalar($value) || is_null($value)) {
|
|
$properties[$key] = $value;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
$connection->update(
|
|
self::TABLE_NAME,
|
|
[
|
|
'tasktype' => $taskType,
|
|
'parameters' => $properties,
|
|
'execution_details' => $executionDetails?->toArray(),
|
|
],
|
|
['uid' => (int)$record['uid']]
|
|
);
|
|
} else {
|
|
// This happens if unserialize() failed (gracefully).
|
|
// Wizard shall not be marked as completed and show up again to let people know.
|
|
$hasFailures = true;
|
|
}
|
|
} catch (\Throwable) {
|
|
// Mark wizard as failed so the upgrade wizard will show up again, and people know there is a problem.
|
|
$hasFailures = true;
|
|
}
|
|
}
|
|
|
|
return !$hasFailures;
|
|
}
|
|
|
|
protected function hasRecordsToUpdate(): bool
|
|
{
|
|
// Check if table exists
|
|
if (!$this->getConnectionPool()->getConnectionForTable(self::TABLE_NAME)->createSchemaManager()->tableExists(self::TABLE_NAME)) {
|
|
return false;
|
|
}
|
|
return (bool)$this->getPreparedQueryBuilder()->count('uid')->executeQuery()->fetchOne();
|
|
}
|
|
|
|
protected function getRecordsToUpdate(): array
|
|
{
|
|
return $this->getPreparedQueryBuilder()->select('*')->executeQuery()->fetchAllAssociative();
|
|
}
|
|
|
|
protected function getPreparedQueryBuilder(): QueryBuilder
|
|
{
|
|
$nativeTaskTypesWithAdditionalFields = $this->getAllNativeTaskTypesWithAdditionalFields();
|
|
|
|
$queryBuilder = $this->getConnectionPool()->getQueryBuilderForTable(self::TABLE_NAME);
|
|
// This is done by intention, so the upgrade wizard continues to work even if we introduce further TCA details for tx_scheduler_task
|
|
$queryBuilder->getRestrictions()->removeAll();
|
|
$queryBuilder
|
|
->from(self::TABLE_NAME)
|
|
->where(
|
|
$queryBuilder->expr()->eq('deleted', $queryBuilder->createNamedParameter(0, ParameterType::INTEGER)),
|
|
$queryBuilder->expr()->or(
|
|
// Find all where the task type is empty (legacy serialized storage)
|
|
// OR where we have a native task type, that contains additional fields we can migrate
|
|
$queryBuilder->expr()->or(
|
|
$queryBuilder->expr()->eq(
|
|
'tasktype',
|
|
$queryBuilder->createNamedParameter('')
|
|
),
|
|
$queryBuilder->expr()->isNull('tasktype')
|
|
),
|
|
$queryBuilder->expr()->and(
|
|
$queryBuilder->expr()->in(
|
|
'tasktype',
|
|
$queryBuilder->createNamedParameter(array_keys($nativeTaskTypesWithAdditionalFields), ArrayParameterType::STRING)
|
|
),
|
|
$queryBuilder->expr()->isNotNull('parameters'),
|
|
)
|
|
)
|
|
);
|
|
|
|
return $queryBuilder;
|
|
}
|
|
|
|
protected function getAllNativeTaskTypesWithAdditionalFields(): array
|
|
{
|
|
$taskService = GeneralUtility::makeInstance(TaskService::class);
|
|
$allTaskInformation = $taskService->getAllTaskTypes();
|
|
$nativeTaskTypesWithAdditionalFields = [];
|
|
foreach ($allTaskInformation as $taskType => $taskInformation) {
|
|
if (($taskInformation['isNativeTask'] ?? false) && $taskInformation['className'] !== ExecuteSchedulableCommandTask::class) {
|
|
// Native tasks can define "additionalFields". However, console commands, which are
|
|
// native tasks as well, do not define real fields but use the "parameters" feature.
|
|
$nativeTaskTypesWithAdditionalFields[$taskType] = $taskInformation['additionalFields'] ?? [];
|
|
}
|
|
}
|
|
return $nativeTaskTypesWithAdditionalFields;
|
|
}
|
|
|
|
protected function getConnectionPool(): ConnectionPool
|
|
{
|
|
return GeneralUtility::makeInstance(ConnectionPool::class);
|
|
}
|
|
}
|