ExportController.php 17.6 KB
Newer Older
1
<?php
2

3
declare(strict_types=1);
4
5
6
7
8
9
10
11
12
13
14
15
16
17

/*
 * 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!
 */

18
19
namespace TYPO3\CMS\Impexp\Controller;

20
use Psr\Http\Message\ResponseFactoryInterface;
21
22
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
23
use TYPO3\CMS\Backend\Template\ModuleTemplate;
24
use TYPO3\CMS\Backend\Template\ModuleTemplateFactory;
25
use TYPO3\CMS\Backend\Utility\BackendUtility;
26
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
Alexander Nitsche's avatar
Alexander Nitsche committed
27
use TYPO3\CMS\Core\Exception as CoreException;
28
use TYPO3\CMS\Core\Imaging\Icon;
29
use TYPO3\CMS\Core\Imaging\IconFactory;
30
31
32
33
34
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Messaging\AbstractMessage;
use TYPO3\CMS\Core\Resource\Folder;
use TYPO3\CMS\Core\Type\Bitmask\Permission;
use TYPO3\CMS\Core\Utility\ArrayUtility;
35
36
37
38
39
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
use TYPO3\CMS\Core\Utility\PathUtility;
use TYPO3\CMS\Impexp\Domain\Repository\PresetRepository;
40
41
42
use TYPO3\CMS\Impexp\Exception\InsufficientUserPermissionsException;
use TYPO3\CMS\Impexp\Exception\MalformedPresetException;
use TYPO3\CMS\Impexp\Exception\PresetNotFoundException;
43
44
45
use TYPO3\CMS\Impexp\Export;

/**
46
 * Export module controller
47
 *
Alexander Nitsche's avatar
Alexander Nitsche committed
48
 * @internal This class is not considered part of the public TYPO3 API.
49
 */
50
class ExportController
51
{
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
    protected array $defaultInputData = [
        'excludeDisabled' => 1,
        'preset' => [],
        'external_static' => [
            'tables' => [],
        ],
        'external_ref' => [
            'tables' => [],
        ],
        'pagetree' => [
            'tables' => [],
        ],
        'extension_dep' => [],
        'meta' => [
            'title' => '',
            'description' => '',
            'notes' => '',
        ],
        'record' => [],
        'list' => [],
    ];

74
    public function __construct(
75
76
77
78
        protected readonly IconFactory $iconFactory,
        protected readonly ModuleTemplateFactory $moduleTemplateFactory,
        protected readonly ResponseFactoryInterface $responseFactory,
        protected readonly PresetRepository $presetRepository
79
    ) {
80
81
    }

82
    public function handleRequest(ServerRequestInterface $request): ResponseInterface
83
    {
84
85
86
87
88
89
90
91
        if ($this->getBackendUser()->isExportEnabled() === false) {
            throw new \RuntimeException(
                'Export module is disabled for non admin users and '
                . 'userTsConfig options.impexp.enableExportForNonAdminUser is not enabled.',
                1636901978
            );
        }

92
93
94
95
96
97
98
99
100
101
        $backendUser = $this->getBackendUser();
        $queryParams = $request->getQueryParams();
        $parsedBody = $request->getParsedBody();

        $id = (int)($parsedBody['id'] ?? $queryParams['id'] ?? 0);
        $permsClause = $backendUser->getPagePermsClause(Permission::PAGE_SHOW);
        $pageInfo = BackendUtility::readPageAccess($id, $permsClause) ?: [];
        if ($pageInfo === []) {
            throw new \RuntimeException("You don't have access to this page.", 1604308206);
        }
102

103
104
105
106
107
108
109
110
111
112
        // @todo: Only small parts of tx_impexp can be hand over as GET, e.g. ['list'] and 'id', drop GET of everything else.
        //        Also, there's a clash with id: it can be ['list']'table:id', it can be 'id', it can be tx_impexp['id']. This
        //        should be de-messed somehow.
        $inputDataFromGetPost = $parsedBody['tx_impexp'] ?? $queryParams['tx_impexp'] ?? [];
        $inputData = $this->defaultInputData;
        ArrayUtility::mergeRecursiveWithOverrule($inputData, $inputDataFromGetPost);
        if ($inputData['resetExclude'] ?? false) {
            $inputData['exclude'] = [];
        }
        $inputData['preset']['public'] = (int)($inputData['preset']['public'] ?? 0);
Alexander Nitsche's avatar
Alexander Nitsche committed
113

114
        $view = $this->moduleTemplateFactory->create($request);
Alexander Nitsche's avatar
Alexander Nitsche committed
115

116
        $presetAction = $parsedBody['preset'] ?? [];
117
        $inputData = $this->processPresets($view, $presetAction, $inputData);
Alexander Nitsche's avatar
Alexander Nitsche committed
118

119
120
        $export = $this->configureExportFromFormData($inputData);
        $export->process();
Alexander Nitsche's avatar
Alexander Nitsche committed
121

122
123
        if ($inputData['download_export'] ?? false) {
            return $this->getDownload($export);
124
        }
125
126
        $saveFolder = $export->getOrCreateDefaultImportExportFolder();
        if (($inputData['save_export'] ?? false) && ($saveFolder instanceof Folder)) {
127
            $this->saveExportToFile($view, $export, $saveFolder);
128
        }
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
        $inputData['filename'] = $export->getExportFileName();

        $view->assignMultiple([
            'id' => $id,
            'errors' => $export->getErrorLog(),
            'preview' => $export->renderPreview(),
            'tableSelectOptions' => $this->getTableSelectOptions(['pages']),
            'treeHTML' => $export->getTreeHTML(),
            'levelSelectOptions' => $this->getPageLevelSelectOptions($inputData),
            'records' => $this->getRecordSelectOptions($inputData),
            'tableList' => $this->getSelectableTableList($inputData),
            'externalReferenceTableSelectOptions' => $this->getTableSelectOptions(),
            'externalStaticTableSelectOptions' => $this->getTableSelectOptions(),
            'presetSelectOptions' => $this->presetRepository->getPresets($id),
            'fileName' => '',
            'filetypeSelectOptions' => $this->getFileSelectOptions($export),
            'saveFolder' => ($saveFolder instanceof Folder) ? $saveFolder->getPublicUrl() : '',
            'hasSaveFolder' => true,
            'extensions' => $this->getExtensionList(),
            'inData' => $inputData,
        ]);
150
151
152
        $view->setModuleName('');
        $view->getDocHeaderComponent()->setMetaInformation($pageInfo);
        return $view->renderResponse('Export');
Alexander Nitsche's avatar
Alexander Nitsche committed
153
    }
154

155
    protected function processPresets(ModuleTemplate $view, array $presetAction, array $inputData): array
Alexander Nitsche's avatar
Alexander Nitsche committed
156
157
    {
        if (empty($presetAction)) {
158
            return $inputData;
Alexander Nitsche's avatar
Alexander Nitsche committed
159
160
161
162
163
        }
        $presetUid = (int)$presetAction['select'];
        try {
            if (isset($presetAction['save'])) {
                if ($presetUid > 0) {
164
165
                    // Update existing
                    $this->presetRepository->updatePreset($presetUid, $inputData);
166
                    $view->addFlashMessage('Preset #' . $presetUid . ' saved!', 'Presets', AbstractMessage::INFO);
167
168
169
                } else {
                    // Insert new
                    $this->presetRepository->createPreset($inputData);
170
                    $view->addFlashMessage('New preset "' . $inputData['preset']['title'] . '" is created', 'Presets', AbstractMessage::INFO);
Alexander Nitsche's avatar
Alexander Nitsche committed
171
172
173
174
175
                }
            }
            if (isset($presetAction['delete'])) {
                if ($presetUid > 0) {
                    $this->presetRepository->deletePreset($presetUid);
176
                    $view->addFlashMessage('Preset #' . $presetUid . ' deleted!', 'Presets', AbstractMessage::INFO);
Alexander Nitsche's avatar
Alexander Nitsche committed
177
                } else {
178
                    $view->addFlashMessage('ERROR: No preset selected for deletion.', 'Presets', AbstractMessage::ERROR);
Alexander Nitsche's avatar
Alexander Nitsche committed
179
180
181
182
183
184
                }
            }
            if (isset($presetAction['load']) || isset($presetAction['merge'])) {
                if ($presetUid > 0) {
                    $presetData = $this->presetRepository->loadPreset($presetUid);
                    if (isset($presetAction['merge'])) {
185
                        // Merge records
Alexander Nitsche's avatar
Alexander Nitsche committed
186
                        if (is_array($presetData['record'] ?? null)) {
187
                            $inputData['record'] = array_merge((array)$inputData['record'], $presetData['record']);
Alexander Nitsche's avatar
Alexander Nitsche committed
188
                        }
189
                        // Merge lists
Alexander Nitsche's avatar
Alexander Nitsche committed
190
                        if (is_array($presetData['list'] ?? null)) {
191
                            $inputData['list'] = array_merge((array)$inputData['list'], $presetData['list']);
Alexander Nitsche's avatar
Alexander Nitsche committed
192
                        }
193
                        $view->addFlashMessage('Preset #' . $presetUid . ' merged!', 'Presets', AbstractMessage::INFO);
Alexander Nitsche's avatar
Alexander Nitsche committed
194
                    } else {
195
                        $inputData = $presetData;
196
                        $view->addFlashMessage('Preset #' . $presetUid . ' loaded!', 'Presets', AbstractMessage::INFO);
Alexander Nitsche's avatar
Alexander Nitsche committed
197
198
                    }
                } else {
199
                    $view->addFlashMessage('ERROR: No preset selected for loading.', 'Presets', AbstractMessage::ERROR);
Alexander Nitsche's avatar
Alexander Nitsche committed
200
201
                }
            }
202
        } catch (PresetNotFoundException|InsufficientUserPermissionsException|MalformedPresetException $e) {
203
            $view->addFlashMessage($e->getMessage(), 'Presets', AbstractMessage::ERROR);
Alexander Nitsche's avatar
Alexander Nitsche committed
204
        }
205
        return $inputData;
206
207
    }

208
    protected function configureExportFromFormData(array $inputData): Export
209
    {
210
211
212
213
214
215
216
217
218
        $export = GeneralUtility::makeInstance(Export::class);
        $export->setExcludeMap((array)($inputData['exclude'] ?? []));
        $export->setSoftrefCfg((array)($inputData['softrefCfg'] ?? []));
        $export->setExtensionDependencies((($inputData['extension_dep'] ?? '') === '') ? [] : (array)$inputData['extension_dep']);
        $export->setShowStaticRelations((bool)($inputData['showStaticRelations'] ?? false));
        $export->setIncludeExtFileResources(!($inputData['excludeHTMLfileResources'] ?? false));
        $export->setExcludeDisabledRecords((bool)($inputData['excludeDisabled'] ?? false));
        if (!empty($inputData['filetype'])) {
            $export->setExportFileType((string)$inputData['filetype']);
Alexander Nitsche's avatar
Alexander Nitsche committed
219
        }
220
221
222
223
224
        $export->setExportFileName($inputData['filename'] ?? '');
        $export->setRelStaticTables($inputData['external_static']['tables']);
        $export->setRelOnlyTables($inputData['external_ref']['tables']);
        if (isset($inputData['save_export'], $inputData['saveFilesOutsideExportFile']) && $inputData['saveFilesOutsideExportFile'] === '1') {
            $export->setSaveFilesOutsideExportFile(true);
225
        }
226
227
228
229
230
231
232
        $export->setTitle($inputData['meta']['title']);
        $export->setDescription($inputData['meta']['description']);
        $export->setNotes($inputData['meta']['notes']);
        $export->setRecord($inputData['record']);
        $export->setList($inputData['list']);
        if (MathUtility::canBeInterpretedAsInteger($inputData['pagetree']['id'] ?? null)) {
            $export->setPid((int)$inputData['pagetree']['id']);
233
        }
234
235
        if (MathUtility::canBeInterpretedAsInteger($inputData['pagetree']['levels'] ?? null)) {
            $export->setLevels((int)$inputData['pagetree']['levels']);
236
        }
237
238
239
        $export->setTables($inputData['pagetree']['tables']);
        return $export;
    }
240

241
242
243
244
245
246
247
248
249
250
251
    protected function getDownload(Export $export): ResponseInterface
    {
        $fileName = $export->getOrGenerateExportFileNameWithFileExtension();
        $fileContent = $export->render();
        $response = $this->responseFactory->createResponse()
            ->withHeader('Content-Type', 'application/octet-stream')
            ->withHeader('Content-Length', (string)strlen($fileContent))
            ->withHeader('Content-Disposition', 'attachment; filename=' . PathUtility::basename($fileName));
        $response->getBody()->write($export->render());
        return $response;
    }
252

253
    protected function saveExportToFile(ModuleTemplate $view, Export $export, Folder $saveFolder): void
254
255
256
257
258
    {
        $languageService = $this->getLanguageService();
        try {
            $saveFile = $export->saveToFile();
            $saveFileSize = $saveFile->getProperty('size');
259
            $view->addFlashMessage(
260
261
262
263
                sprintf($languageService->sL('LLL:EXT:impexp/Resources/Private/Language/locallang.xlf:exportdata_savedInSBytes'), $saveFile->getPublicUrl(), GeneralUtility::formatSize($saveFileSize)),
                $languageService->sL('LLL:EXT:impexp/Resources/Private/Language/locallang.xlf:exportdata_savedFile')
            );
        } catch (CoreException $e) {
264
            $view->addFlashMessage(
265
266
267
268
                sprintf($languageService->sL('LLL:EXT:impexp/Resources/Private/Language/locallang.xlf:exportdata_badPathS'), $saveFolder->getPublicUrl()),
                $languageService->sL('LLL:EXT:impexp/Resources/Private/Language/locallang.xlf:exportdata_problemsSavingFile'),
                AbstractMessage::ERROR
            );
269
270
271
        }
    }

272
    protected function getPageLevelSelectOptions(array $inputData): array
273
    {
274
275
276
        $languageService = $this->getLanguageService();
        $options = [];
        if (MathUtility::canBeInterpretedAsInteger($inputData['pagetree']['id'] ?? '')) {
Alexander Nitsche's avatar
Alexander Nitsche committed
277
            $options = [
278
279
280
281
282
283
284
285
                Export::LEVELS_RECORDS_ON_THIS_PAGE => $languageService->sL('LLL:EXT:impexp/Resources/Private/Language/locallang.xlf:makeconfig_tablesOnThisPage'),
                Export::LEVELS_EXPANDED_TREE => $languageService->sL('LLL:EXT:impexp/Resources/Private/Language/locallang.xlf:makeconfig_expandedTree'),
                0 => $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.depth_0'),
                1 => $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.depth_1'),
                2 => $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.depth_2'),
                3 => $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.depth_3'),
                4 => $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.depth_4'),
                Export::LEVELS_INFINITE => $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.depth_infi'),
286
287
            ];
        }
288
289
        return $options;
    }
Alexander Nitsche's avatar
Alexander Nitsche committed
290

291
292
293
294
295
296
297
298
299
300
301
302
    protected function getRecordSelectOptions(array $inputData): array
    {
        $records = [];
        foreach ($inputData['record'] as $tableNameColonUid) {
            [$tableName, $recordUid] = explode(':', $tableNameColonUid);
            if ($record = BackendUtility::getRecordWSOL((string)$tableName, (int)$recordUid)) {
                $records[] = [
                    'icon' => $this->iconFactory->getIconForRecord($tableName, $record, Icon::SIZE_SMALL)->render(),
                    'title' => BackendUtility::getRecordTitle($tableName, $record, true),
                    'tableName' => $tableName,
                    'recordUid' => $recordUid,
                ];
303
304
            }
        }
305
306
        return $records;
    }
307

308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
    protected function getSelectableTableList(array $inputData): array
    {
        $backendUser = $this->getBackendUser();
        $languageService = $this->getLanguageService();
        $tableList = [];
        foreach ($inputData['list'] as $reference) {
            $referenceParts = explode(':', $reference);
            $tableName = $referenceParts[0];
            if ($backendUser->check('tables_select', $tableName)) {
                // If the page is actually the root, handle it differently.
                // NOTE: we don't compare integers, because the number comes from the split string above
                if ($referenceParts[1] === '0') {
                    $iconAndTitle = $this->iconFactory->getIcon('apps-pagetree-root', Icon::SIZE_SMALL)->render() . $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'];
                } else {
                    $record = BackendUtility::getRecordWSOL('pages', (int)$referenceParts[1]);
                    $iconAndTitle = $this->iconFactory->getIconForRecord('pages', $record, Icon::SIZE_SMALL)->render()
                        . BackendUtility::getRecordTitle('pages', $record, true);
325
                }
326
327
328
329
                $tableList[] = [
                    'iconAndTitle' => sprintf($languageService->sL('LLL:EXT:impexp/Resources/Private/Language/locallang.xlf:makeconfig_tableListEntry'), $tableName, $iconAndTitle),
                    'reference' => $reference,
                ];
330
331
            }
        }
332
        return $tableList;
333
334
    }

335
    protected function getExtensionList(): array
336
337
    {
        $loadedExtensions = ExtensionManagementUtility::getLoadedExtensionListArray();
338
        return array_combine($loadedExtensions, $loadedExtensions);
339
340
    }

341
    protected function getFileSelectOptions(Export $export): array
342
    {
343
        $languageService = $this->getLanguageService();
Alexander Nitsche's avatar
Alexander Nitsche committed
344
        $fileTypeOptions = [];
345
346
        foreach ($export->getSupportedFileTypes() as $supportedFileType) {
            $fileTypeOptions[$supportedFileType] = $languageService->sL('LLL:EXT:impexp/Resources/Private/Language/locallang.xlf:makesavefo_' . $supportedFileType);
347
        }
348
        return $fileTypeOptions;
349
350
351
    }

    /**
352
353
     * Get a list of all exportable tables - basically all TCA tables. Blacklist some if wanted.
     * Returned array keys are table names, values are "translations".
354
     */
355
    protected function getTableSelectOptions(array $excludeList = []): array
356
    {
357
358
359
360
361
362
363
364
        $languageService = $this->getLanguageService();
        $backendUser = $this->getBackendUser();
        $options = [
            '_ALL' => $languageService->sL('LLL:EXT:impexp/Resources/Private/Language/locallang.xlf:ALL_tables'),
        ];
        $availableTables = array_keys($GLOBALS['TCA']);
        foreach ($availableTables as $table) {
            if (!in_array($table, $excludeList, true) && $backendUser->check('tables_select', $table)) {
Alexander Nitsche's avatar
Alexander Nitsche committed
365
                $options[$table] = $table;
366
367
            }
        }
Alexander Nitsche's avatar
Alexander Nitsche committed
368
369
        natsort($options);
        return $options;
370
    }
371
372
373
374
375
376
377
378
379
380

    protected function getBackendUser(): BackendUserAuthentication
    {
        return $GLOBALS['BE_USER'];
    }

    protected function getLanguageService(): LanguageService
    {
        return $GLOBALS['LANG'];
    }
381
}