Passed
Push — master ( 6d956e...2f0c26 )
by
unknown
13:12
created

TcaMigration::migrateFileFolderConfiguration()   B

Complexity

Conditions 9
Paths 3

Size

Total Lines 34
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 23
dl 0
loc 34
rs 8.0555
c 0
b 0
f 0
cc 9
nc 3
nop 1
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the TYPO3 CMS project.
7
 *
8
 * It is free software; you can redistribute it and/or modify it under
9
 * the terms of the GNU General Public License, either version 2
10
 * of the License, or any later version.
11
 *
12
 * For the full copyright and license information, please read the
13
 * LICENSE.txt file that was distributed with this source code.
14
 *
15
 * The TYPO3 project - inspiring people to share!
16
 */
17
18
namespace TYPO3\CMS\Core\Migrations;
19
20
/**
21
 * Migrate TCA from old to new syntax. Used in bootstrap and Flex Form Data Structures.
22
 *
23
 * @internal Class and API may change any time.
24
 */
25
class TcaMigration
26
{
27
    /**
28
     * Accumulate migration messages
29
     *
30
     * @var array
31
     */
32
    protected $messages = [];
33
34
    /**
35
     * Run some general TCA validations, then migrate old TCA to new TCA.
36
     *
37
     * This class is typically called within bootstrap with empty caches after all TCA
38
     * files from extensions have been loaded. The migration is then applied and
39
     * the migrated result is cached.
40
     * For flex form TCA, this class is called dynamically if opening a record in the backend.
41
     *
42
     * See unit tests for details.
43
     *
44
     * @param array $tca
45
     * @return array
46
     */
47
    public function migrate(array $tca): array
48
    {
49
        $this->validateTcaType($tca);
50
51
        $tca = $this->migrateColumnsConfig($tca);
52
        $tca = $this->migrateLocalizeChildrenAtParentLocalization($tca);
53
        $tca = $this->migratePagesLanguageOverlayRemoval($tca);
54
        $tca = $this->removeSelIconFieldPath($tca);
55
        $tca = $this->removeSetToDefaultOnCopy($tca);
56
        $tca = $this->sanitizeControlSectionIntegrity($tca);
57
        $tca = $this->removeEnableMultiSelectFilterTextfieldConfiguration($tca);
58
        $tca = $this->removeExcludeFieldForTransOrigPointerField($tca);
59
        $tca = $this->removeShowRecordFieldListField($tca);
60
        $tca = $this->removeWorkspacePlaceholderShadowColumnsConfiguration($tca);
61
        $tca = $this->migrateLanguageFieldToTcaTypeLanguage($tca);
62
        $tca = $this->migrateSpecialLanguagesToTcaTypeLanguage($tca);
63
        $tca = $this->removeShowRemovedLocalizationRecords($tca);
64
        $tca = $this->migrateFileFolderConfiguration($tca);
65
66
        return $tca;
67
    }
68
69
    /**
70
     * Get messages of migrated fields. Can be used for deprecation messages after migrate() was called.
71
     *
72
     * @return array Migration messages
73
     */
74
    public function getMessages(): array
75
    {
76
        return $this->messages;
77
    }
78
79
    /**
80
     * Check for required TCA configuration
81
     *
82
     * @param array $tca Incoming TCA
83
     */
84
    protected function validateTcaType(array $tca)
85
    {
86
        foreach ($tca as $table => $tableDefinition) {
87
            if (!isset($tableDefinition['columns']) || !is_array($tableDefinition['columns'])) {
88
                continue;
89
            }
90
            foreach ($tableDefinition['columns'] as $fieldName => $fieldConfig) {
91
                if (isset($fieldConfig['config']) && is_array($fieldConfig['config']) && empty($fieldConfig['config']['type'])) {
92
                    throw new \UnexpectedValueException(
93
                        'Missing "type" in TCA of field "[\'' . $table . '\'][\'' . $fieldName . '\'][\'config\']".',
94
                        1482394401
95
                    );
96
                }
97
            }
98
        }
99
    }
100
101
    /**
102
     * Find columns fields that don't have a 'config' section at all, add
103
     * ['config']['type'] = 'none'; for those to enforce config
104
     *
105
     * @param array $tca Incoming TCA
106
     * @return array
107
     */
108
    protected function migrateColumnsConfig(array $tca): array
109
    {
110
        foreach ($tca as $table => &$tableDefinition) {
111
            if (!isset($tableDefinition['columns']) || !is_array($tableDefinition['columns'])) {
112
                continue;
113
            }
114
            foreach ($tableDefinition['columns'] as $fieldName => &$fieldConfig) {
115
                if ((!isset($fieldConfig['config']) || !is_array($fieldConfig['config'])) && !isset($fieldConfig['type'])) {
116
                    $fieldConfig['config'] = [
117
                        'type' => 'none',
118
                    ];
119
                    $this->messages[] = 'TCA table "' . $table . '" columns field "' . $fieldName . '"'
120
                        . ' had no mandatory "config" section. This has been added with default type "none":'
121
                        . ' TCA "' . $table . '[\'columns\'][\'' . $fieldName . '\'][\'config\'][\'type\'] = \'none\'"';
122
                }
123
            }
124
        }
125
        return $tca;
126
    }
127
128
    /**
129
     * Option $TCA[$table]['columns'][$columnName]['config']['behaviour']['localizeChildrenAtParentLocalization']
130
     * is always on, so this option can be removed.
131
     *
132
     * @param array $tca
133
     * @return array the modified TCA structure
134
     */
135
    protected function migrateLocalizeChildrenAtParentLocalization(array $tca): array
136
    {
137
        foreach ($tca as $table => &$tableDefinition) {
138
            if (!isset($tableDefinition['columns']) || !is_array($tableDefinition['columns'])) {
139
                continue;
140
            }
141
            foreach ($tableDefinition['columns'] as $fieldName => &$fieldConfig) {
142
                if (($fieldConfig['config']['type'] ?? null) !== 'inline') {
143
                    continue;
144
                }
145
146
                $localizeChildrenAtParentLocalization = ($fieldConfig['config']['behaviour']['localizeChildrenAtParentLocalization'] ?? null);
147
                if ($localizeChildrenAtParentLocalization === null) {
148
                    continue;
149
                }
150
151
                if ($localizeChildrenAtParentLocalization) {
152
                    $this->messages[] = 'The TCA setting \'localizeChildrenAtParentLocalization\' is deprecated '
153
                        . ' and should be removed from TCA for ' . $table . '[\'columns\']'
154
                        . '[\'' . $fieldName . '\'][\'config\'][\'behaviour\'][\'localizeChildrenAtParentLocalization\']';
155
                } else {
156
                    $this->messages[] = 'The TCA setting \'localizeChildrenAtParentLocalization\' is deprecated '
157
                        . ', as this functionality is always enabled. The option should be removed from TCA for '
158
                        . $table . '[\'columns\'][\'' . $fieldName . '\'][\'config\'][\'behaviour\']'
159
                        . '[\'localizeChildrenAtParentLocalization\']';
160
                }
161
                unset($fieldConfig['config']['behaviour']['localizeChildrenAtParentLocalization']);
162
            }
163
        }
164
        return $tca;
165
    }
166
167
    /**
168
     * Removes $TCA['pages_language_overlay'] if defined.
169
     *
170
     * @param array $tca
171
     * @return array the modified TCA structure
172
     */
173
    protected function migratePagesLanguageOverlayRemoval(array $tca)
174
    {
175
        if (isset($tca['pages_language_overlay'])) {
176
            $this->messages[] = 'The TCA table \'pages_language_overlay\' is'
177
                . ' not used anymore and has been removed automatically in'
178
                . ' order to avoid negative side-effects.';
179
            unset($tca['pages_language_overlay']);
180
        }
181
        return $tca;
182
    }
183
184
    /**
185
     * Removes configuration removeEnableMultiSelectFilterTextfield
186
     *
187
     * @param array $tca
188
     * @return array the modified TCA structure
189
     */
190
    protected function removeEnableMultiSelectFilterTextfieldConfiguration(array $tca): array
191
    {
192
        foreach ($tca as $table => &$tableDefinition) {
193
            if (!isset($tableDefinition['columns']) || !is_array($tableDefinition['columns'])) {
194
                continue;
195
            }
196
            foreach ($tableDefinition['columns'] as $fieldName => &$fieldConfig) {
197
                if (!isset($fieldConfig['config']['enableMultiSelectFilterTextfield'])) {
198
                    continue;
199
                }
200
201
                $this->messages[] = 'The TCA setting \'enableMultiSelectFilterTextfield\' is deprecated '
202
                    . ' and should be removed from TCA for ' . $table . '[\'columns\']'
203
                    . '[\'' . $fieldName . '\'][\'config\'][\'enableMultiSelectFilterTextfield\']';
204
                unset($fieldConfig['config']['enableMultiSelectFilterTextfield']);
205
            }
206
        }
207
        return $tca;
208
    }
209
210
    /**
211
     * Removes $TCA[$mytable][ctrl][selicon_field_path]
212
     *
213
     * @param array $tca
214
     * @return array the modified TCA structure
215
     */
216
    protected function removeSelIconFieldPath(array $tca): array
217
    {
218
        foreach ($tca as $table => &$configuration) {
219
            if (isset($configuration['ctrl']['selicon_field_path'])) {
220
                $this->messages[] = 'The TCA table \'' . $table . '\' defines '
221
                    . '[ctrl][selicon_field_path] which should be removed from TCA, '
222
                    . 'as it is not in use anymore.';
223
                unset($configuration['ctrl']['selicon_field_path']);
224
            }
225
        }
226
        return $tca;
227
    }
228
229
    /**
230
     * Removes $TCA[$mytable][ctrl][setToDefaultOnCopy]
231
     *
232
     * @param array $tca
233
     * @return array the modified TCA structure
234
     */
235
    protected function removeSetToDefaultOnCopy(array $tca): array
236
    {
237
        foreach ($tca as $table => &$configuration) {
238
            if (isset($configuration['ctrl']['setToDefaultOnCopy'])) {
239
                $this->messages[] = 'The TCA table \'' . $table . '\' defines '
240
                    . '[ctrl][setToDefaultOnCopy] which should be removed from TCA, '
241
                    . 'as it is not in use anymore.';
242
                unset($configuration['ctrl']['setToDefaultOnCopy']);
243
            }
244
        }
245
        return $tca;
246
    }
247
248
    /**
249
     * Ensures that system internal columns that are required for data integrity
250
     * (e.g. localize or copy a record) are available in case they have been defined
251
     * in $GLOBALS['TCA'][<table-name>]['ctrl'].
252
     *
253
     * The list of references to usages below is not necessarily complete.
254
     *
255
     * @param array $tca
256
     * @return array
257
     *
258
     * @see \TYPO3\CMS\Core\DataHandling\DataHandler::fillInFieldArray()
259
     */
260
    protected function sanitizeControlSectionIntegrity(array $tca): array
261
    {
262
        $defaultControlSectionColumnConfig = [
263
            'type' => 'passthrough',
264
            'default' => 0,
265
        ];
266
        $controlSectionNames = [
267
            'origUid' => $defaultControlSectionColumnConfig,
268
            'languageField' => [
269
                'type' => 'language'
270
            ],
271
            'transOrigPointerField' => $defaultControlSectionColumnConfig,
272
            'translationSource' => $defaultControlSectionColumnConfig,
273
        ];
274
275
        foreach ($tca as $tableName => &$configuration) {
276
            foreach ($controlSectionNames as $controlSectionName => $controlSectionColumnConfig) {
277
                $columnName = $configuration['ctrl'][$controlSectionName] ?? null;
278
                if (empty($columnName) || !empty($configuration['columns'][$columnName])) {
279
                    continue;
280
                }
281
                $configuration['columns'][$columnName] = [
282
                    'config' => $controlSectionColumnConfig
283
                ];
284
            }
285
        }
286
        return $tca;
287
    }
288
289
    /**
290
     * Removes $TCA[$mytable][columns][_transOrigPointerField_][exclude] if defined
291
     *
292
     * @param array $tca
293
     *
294
     * @return array
295
     */
296
    protected function removeExcludeFieldForTransOrigPointerField(array $tca): array
297
    {
298
        foreach ($tca as $table => &$configuration) {
299
            if (isset($configuration['ctrl']['transOrigPointerField'],
300
                $configuration['columns'][$configuration['ctrl']['transOrigPointerField']]['exclude'])
301
            ) {
302
                $this->messages[] = 'The \'' . $table . '\' TCA tables transOrigPointerField '
303
                    . '\'' . $configuration['ctrl']['transOrigPointerField'] . '\' is defined '
304
                    . ' as excluded field which is no longer needed and should therefore be removed. ';
305
                unset($configuration['columns'][$configuration['ctrl']['transOrigPointerField']]['exclude']);
306
            }
307
        }
308
309
        return $tca;
310
    }
311
312
    /**
313
     * Removes $TCA[$mytable]['interface']['showRecordFieldList'] and also $TCA[$mytable]['interface']
314
     * if `showRecordFieldList` was the only key in the array.
315
     *
316
     * @param array $tca
317
     * @return array
318
     */
319
    protected function removeShowRecordFieldListField(array $tca): array
320
    {
321
        foreach ($tca as $table => &$configuration) {
322
            if (!isset($configuration['interface']['showRecordFieldList'])) {
323
                continue;
324
            }
325
            $this->messages[] = 'The \'' . $table . '\' TCA configuration \'showRecordFieldList\''
326
                . ' inside the section \'interface\' is not evaluated anymore and should therefore be removed.';
327
            unset($configuration['interface']['showRecordFieldList']);
328
            if ($configuration['interface'] === []) {
329
                unset($configuration['interface']);
330
            }
331
        }
332
333
        return $tca;
334
    }
335
336
    /**
337
     * Removes $TCA[$mytable][ctrl][shadowColumnsForMovePlaceholders]
338
     * and $TCA[$mytable][ctrl][shadowColumnsForNewPlaceholders]
339
     *
340
     * @param array $tca
341
     * @return array the modified TCA structure
342
     */
343
    protected function removeWorkspacePlaceholderShadowColumnsConfiguration(array $tca): array
344
    {
345
        foreach ($tca as $table => &$configuration) {
346
            if (isset($configuration['ctrl']['shadowColumnsForNewPlaceholders'])) {
347
                $this->messages[] = 'The TCA table \'' . $table . '\' defines '
348
                    . '[ctrl][shadowColumnsForNewPlaceholders] which should be removed from TCA, '
349
                    . 'as it is not in use anymore.';
350
                unset($configuration['ctrl']['shadowColumnsForNewPlaceholders']);
351
            }
352
            if (isset($configuration['ctrl']['shadowColumnsForMovePlaceholders'])) {
353
                $this->messages[] = 'The TCA table \'' . $table . '\' defines '
354
                    . '[ctrl][shadowColumnsForMovePlaceholders] which should be removed from TCA, '
355
                    . 'as it is not in use anymore.';
356
                unset($configuration['ctrl']['shadowColumnsForMovePlaceholders']);
357
            }
358
        }
359
        return $tca;
360
    }
361
362
    /**
363
     * Replaces $TCA[$mytable][columns][$TCA[$mytable][ctrl][languageField]][config] with
364
     * $TCA[$mytable][columns][$TCA[$mytable][ctrl][languageField]][config][type] = 'language'
365
     *
366
     * @param array $tca
367
     * @return array
368
     */
369
    protected function migrateLanguageFieldToTcaTypeLanguage(array $tca): array
370
    {
371
        foreach ($tca as $table => &$configuration) {
372
            if (isset($configuration['ctrl']['languageField'], $configuration['columns'][$configuration['ctrl']['languageField']])
373
                && ($configuration['columns'][$configuration['ctrl']['languageField']]['config']['type'] ?? '') !== 'language'
374
            ) {
375
                $this->messages[] = 'The TCA field \'' . $configuration['ctrl']['languageField'] . '\' '
376
                    . 'of table \'' . $table . '\' is defined as the \'languageField\' and should '
377
                    . 'therefore use the TCA type \'language\' instead of TCA type \'select\' with '
378
                    . '\'foreign_table=sys_language\' or \'special=languages\'.';
379
                $configuration['columns'][$configuration['ctrl']['languageField']]['config'] = [
380
                    'type' => 'language'
381
                ];
382
            }
383
        }
384
385
        return $tca;
386
    }
387
388
    /**
389
     * Replaces $TCA[$mytable][columns][field][config][special] = 'languages' with
390
     * $TCA[$mytable][columns][field][config][type] = 'language'
391
     *
392
     * @param array $tca
393
     * @return array
394
     */
395
    protected function migrateSpecialLanguagesToTcaTypeLanguage(array $tca): array
396
    {
397
        foreach ($tca as $table => &$tableDefinition) {
398
            if (!isset($tableDefinition['columns']) || !is_array($tableDefinition['columns'])) {
399
                continue;
400
            }
401
            foreach ($tableDefinition['columns'] as $fieldName => &$fieldConfig) {
402
                if ((string)($fieldConfig['config']['type'] ?? '') !== 'select'
403
                    || (string)($fieldConfig['config']['special'] ?? '') !== 'languages'
404
                ) {
405
                    continue;
406
                }
407
                $this->messages[] = 'The TCA field \'' . $fieldName . '\' of table \'' . $table . '\' is '
408
                    . 'defined as type \'select\' with the \'special=languages\' option. This is not '
409
                    . 'evaluated anymore and should be replaced by the TCA type \'language\'.';
410
                $fieldConfig['config'] = [
411
                    'type' => 'language'
412
                ];
413
            }
414
        }
415
416
        return $tca;
417
    }
418
419
    protected function removeShowRemovedLocalizationRecords(array $tca): array
420
    {
421
        foreach ($tca as $table => &$tableDefinition) {
422
            if (!isset($tableDefinition['columns']) || !is_array($tableDefinition['columns'])) {
423
                continue;
424
            }
425
            foreach ($tableDefinition['columns'] as $fieldName => &$fieldConfig) {
426
                if ((string)($fieldConfig['config']['type'] ?? '') !== 'inline'
427
                    || !isset($fieldConfig['config']['appearance']['showRemovedLocalizationRecords'])
428
                ) {
429
                    continue;
430
                }
431
                $this->messages[] = 'The TCA field \'' . $fieldName . '\' of table \'' . $table . '\' is '
432
                    . 'defined as type \'inline\' with the \'appearance.showRemovedLocalizationRecords\' option set. '
433
                    . 'As this option is not evaluated anymore and no replacement exists, it should be removed from TCA.';
434
                unset($fieldConfig['config']['appearance']['showRemovedLocalizationRecords']);
435
            }
436
        }
437
438
        return $tca;
439
    }
440
441
    /**
442
     * Moves the "fileFolder" configuration of TCA columns type=select
443
     * into sub array "fileFolderConfig", while renaming those options.
444
     *
445
     * @param array $tca
446
     * @return array
447
     */
448
    protected function migrateFileFolderConfiguration(array $tca): array
449
    {
450
        foreach ($tca as $table => &$tableDefinition) {
451
            if (!isset($tableDefinition['columns']) || !is_array($tableDefinition['columns'])) {
452
                continue;
453
            }
454
            foreach ($tableDefinition['columns'] as $fieldName => &$fieldConfig) {
455
                if ((string)($fieldConfig['config']['type'] ?? '') !== 'select'
456
                    || !isset($fieldConfig['config']['fileFolder'])
457
                ) {
458
                    continue;
459
                }
460
                $fieldConfig['config']['fileFolderConfig'] = [
461
                    'folder' => $fieldConfig['config']['fileFolder']
462
                ];
463
                unset($fieldConfig['config']['fileFolder']);
464
                if (isset($fieldConfig['config']['fileFolder_extList'])) {
465
                    $fieldConfig['config']['fileFolderConfig']['allowedExtensions'] = $fieldConfig['config']['fileFolder_extList'];
466
                    unset($fieldConfig['config']['fileFolder_extList']);
467
                }
468
                if (isset($fieldConfig['config']['fileFolder_recursions'])) {
469
                    $fieldConfig['config']['fileFolderConfig']['depth'] = $fieldConfig['config']['fileFolder_recursions'];
470
                    unset($fieldConfig['config']['fileFolder_recursions']);
471
                }
472
                $this->messages[] = 'The TCA field \'' . $fieldName . '\' of table \'' . $table . '\' is '
473
                    . 'defined as type \'select\' with the \'fileFolder\' configuration option set. To streamline '
474
                    . 'the configuration, all \'fileFolder\' related configuration options were moved into a '
475
                    . 'dedicated sub array \'fileFolderConfig\', while \'fileFolder\' is now just \'folder\' and '
476
                    . 'the other options have been renamed to \'allowedExtensions\' and \'depth\'. '
477
                    . 'The TCA configuration should be adjusted accordingly.';
478
            }
479
        }
480
481
        return $tca;
482
    }
483
}
484