Passed
Push — master ( 62e46d...e4833f )
by
unknown
15:08
created

ShortcutRecordsMigration::updateNecessary()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 1
c 1
b 0
f 0
dl 0
loc 3
rs 10
cc 2
nc 2
nop 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace TYPO3\CMS\Install\Updates;
6
7
/*
8
 * This file is part of the TYPO3 CMS project.
9
 *
10
 * It is free software; you can redistribute it and/or modify it under
11
 * the terms of the GNU General Public License, either version 2
12
 * of the License, or any later version.
13
 *
14
 * For the full copyright and license information, please read the
15
 * LICENSE.txt file that was distributed with this source code.
16
 *
17
 * The TYPO3 project - inspiring people to share!
18
 */
19
20
use TYPO3\CMS\Backend\Routing\Router;
21
use TYPO3\CMS\Core\Database\ConnectionPool;
22
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
23
use TYPO3\CMS\Core\Utility\GeneralUtility;
24
25
/**
26
 * @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
27
 */
28
class ShortcutRecordsMigration implements UpgradeWizardInterface
29
{
30
    private const TABLE_NAME = 'sys_be_shortcuts';
31
32
    protected ?iterable $routes = null;
33
34
    public function getIdentifier(): string
35
    {
36
        return 'shortcutRecordsMigration';
37
    }
38
39
    public function getTitle(): string
40
    {
41
        return 'Migrate shortcut records to new format.';
42
    }
43
44
    public function getDescription(): string
45
    {
46
        return 'To support speaking urls in the backend, some fields need to be changed in sys_be_shortcuts.';
47
    }
48
49
    public function getPrerequisites(): array
50
    {
51
        return [
52
            DatabaseUpdatedPrerequisite::class
53
        ];
54
    }
55
56
    public function updateNecessary(): bool
57
    {
58
        return $this->columnsExistInTable() && $this->hasRecordsToUpdate();
59
    }
60
61
    public function executeUpdate(): bool
62
    {
63
        $this->routes = GeneralUtility::makeInstance(Router::class)->getRoutes();
64
        $connection = $this->getConnectionPool()->getConnectionForTable(self::TABLE_NAME);
65
66
        foreach ($this->getRecordsToUpdate() as $record) {
67
            [$moduleName] = explode('|', (string)$record['module_name'], 2);
68
69
            if (!is_string($moduleName) || $moduleName === '') {
70
                continue;
71
            }
72
73
            if (($routeIdentifier = $this->getRouteIdentifierForModuleName($moduleName)) === '') {
74
                continue;
75
            }
76
77
            // Parse the url and reveal the arguments (query parameters)
78
            $parsedUrl = parse_url((string)$record['url']) ?: [];
79
            $arguments = [];
80
            parse_str($parsedUrl['query'] ?? '', $arguments);
81
82
            // Unset not longer needed arguments
83
            unset($arguments['route'], $arguments['returnUrl']);
84
85
            try {
86
                $encodedArguments = json_encode($arguments, JSON_THROW_ON_ERROR) ?: '';
87
            } catch (\JsonException $e) {
88
                // Skip the row if arguments can not be encoded
89
                continue;
90
            }
91
92
            // Update the record - Note: The "old" values won't be unset
93
            $connection->update(
94
                self::TABLE_NAME,
95
                ['route' => $routeIdentifier, 'arguments' => $encodedArguments],
96
                ['uid' => (int)$record['uid']]
97
            );
98
        }
99
100
        return true;
101
    }
102
103
    protected function columnsExistInTable(): bool
104
    {
105
        $schemaManager = $this->getConnectionPool()->getConnectionForTable(self::TABLE_NAME)->getSchemaManager();
106
107
        if ($schemaManager === null) {
108
            return false;
109
        }
110
111
        $tableColumns = $schemaManager->listTableColumns(self::TABLE_NAME);
112
113
        foreach (['module_name', 'url', 'route', 'arguments'] as $column) {
114
            if (!isset($tableColumns[$column])) {
115
                return false;
116
            }
117
        }
118
119
        return true;
120
    }
121
122
    protected function hasRecordsToUpdate(): bool
123
    {
124
        return (bool)$this->getPreparedQueryBuilder()->count('uid')->execute()->fetchColumn();
125
    }
126
127
    protected function getRecordsToUpdate(): array
128
    {
129
        return $this->getPreparedQueryBuilder()->select(...['uid', 'module_name', 'url'])->execute()->fetchAll();
130
    }
131
132
    protected function getPreparedQueryBuilder(): QueryBuilder
133
    {
134
        $queryBuilder = $this->getConnectionPool()->getQueryBuilderForTable(self::TABLE_NAME);
135
        $queryBuilder->getRestrictions()->removeAll();
136
        $queryBuilder
137
            ->from(self::TABLE_NAME)
138
            ->where(
139
                $queryBuilder->expr()->neq('module_name', $queryBuilder->createNamedParameter('')),
140
                $queryBuilder->expr()->andX(
141
                    $queryBuilder->expr()->neq('url', $queryBuilder->createNamedParameter('')),
142
                    $queryBuilder->expr()->isNotNull('url')
143
                ),
144
                $queryBuilder->expr()->eq('route', $queryBuilder->createNamedParameter('')),
145
                $queryBuilder->expr()->orX(
146
                    $queryBuilder->expr()->eq('arguments', $queryBuilder->createNamedParameter('')),
147
                    $queryBuilder->expr()->isNull('arguments')
148
                )
149
            );
150
151
        return $queryBuilder;
152
    }
153
154
    protected function getRouteIdentifierForModuleName(string $moduleName): string
155
    {
156
        // Check static special cases first
157
        switch ($moduleName) {
158
            case 'xMOD_alt_doc.php':
159
                return 'record_edit';
160
            case 'file_edit':
161
            case 'wizard_rte':
162
                return $moduleName;
163
        }
164
165
        // Get identifier from module configuration
166
        $routeIdentifier = $GLOBALS['TBE_MODULES']['_configuration'][$moduleName]['id'] ?? $moduleName;
167
168
        // Check if a route with the identifier exist
169
        if (isset($this->routes[$routeIdentifier])
170
            && $this->routes[$routeIdentifier]->hasOption('moduleName')
171
            && $this->routes[$routeIdentifier]->getOption('moduleName') === $moduleName
172
        ) {
173
            return $routeIdentifier;
174
        }
175
176
        // If the defined route identifier can't be fetched, try from the other side
177
        // by iterating over the routes to match a route by the defined module name
178
        foreach ($this->routes as $identifier => $route) {
179
            if ($route->hasOption('moduleName') && $route->getOption('moduleName') === $moduleName) {
180
                return $routeIdentifier;
181
            }
182
        }
183
184
        return '';
185
    }
186
187
    protected function getConnectionPool(): ConnectionPool
188
    {
189
        return GeneralUtility::makeInstance(ConnectionPool::class);
190
    }
191
}
192