1<?php
2namespace TYPO3\CMS\Lowlevel\Controller;
3
4/*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17use Psr\Http\Message\ResponseInterface;
18use Psr\Http\Message\ServerRequestInterface;
19use TYPO3\CMS\Backend\Template\Components\ButtonBar;
20use TYPO3\CMS\Backend\Template\ModuleTemplate;
21use TYPO3\CMS\Backend\Utility\BackendUtility;
22use TYPO3\CMS\Core\Database\QueryView;
23use TYPO3\CMS\Core\Database\ReferenceIndex;
24use TYPO3\CMS\Core\Http\HtmlResponse;
25use TYPO3\CMS\Core\Imaging\Icon;
26use TYPO3\CMS\Core\Imaging\IconFactory;
27use TYPO3\CMS\Core\Localization\LanguageService;
28use TYPO3\CMS\Core\Page\PageRenderer;
29use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
30use TYPO3\CMS\Core\Utility\GeneralUtility;
31use TYPO3\CMS\Core\Utility\PathUtility;
32use TYPO3\CMS\Fluid\View\StandaloneView;
33use TYPO3\CMS\Lowlevel\Integrity\DatabaseIntegrityCheck;
34
35/**
36 * Script class for the DB int module
37 * @internal This class is a specific Backend controller implementation and is not part of the TYPO3's Core API.
38 */
39class DatabaseIntegrityController
40{
41    /**
42     * @var string
43     */
44    protected $formName = 'queryform';
45
46    /**
47     * The name of the module
48     *
49     * @var string
50     */
51    protected $moduleName = 'system_dbint';
52
53    /**
54     * @var StandaloneView
55     */
56    protected $view;
57
58    /**
59     * @var string
60     */
61    protected $templatePath = 'EXT:lowlevel/Resources/Private/Templates/Backend/';
62
63    /**
64     * @var IconFactory
65     */
66    protected $iconFactory;
67
68    /**
69     * ModuleTemplate Container
70     *
71     * @var ModuleTemplate
72     */
73    protected $moduleTemplate;
74
75    /**
76     * Loaded with the global array $MCONF which holds some module configuration from the conf.php file of backend modules.
77     *
78     * @see init()
79     * @var array
80     */
81    protected $MCONF = [
82        'name' => 'system_dbint',
83    ];
84
85    /**
86     * The module menu items array. Each key represents a key for which values can range between the items in the array of that key.
87     *
88     * @see init()
89     * @var array
90     */
91    protected $MOD_MENU = [
92        'function' => []
93    ];
94
95    /**
96     * Current settings for the keys of the MOD_MENU array
97     *
98     * @see $MOD_MENU
99     * @var array
100     */
101    protected $MOD_SETTINGS = [];
102
103    /**
104     * Injects the request object for the current request or subrequest
105     * Simply calls main() and init() and outputs the content
106     *
107     * @param ServerRequestInterface $request the current request
108     * @return ResponseInterface the response with the content
109     */
110    public function mainAction(ServerRequestInterface $request): ResponseInterface
111    {
112        $this->getLanguageService()->includeLLFile('EXT:lowlevel/Resources/Private/Language/locallang.xlf');
113        $this->iconFactory = GeneralUtility::makeInstance(IconFactory::class);
114        $this->view = GeneralUtility::makeInstance(StandaloneView::class);
115        $this->view->getRequest()->setControllerExtensionName('lowlevel');
116
117        $this->menuConfig();
118        $this->moduleTemplate = GeneralUtility::makeInstance(ModuleTemplate::class);
119        $this->moduleTemplate->addJavaScriptCode(
120            'jumpToUrl',
121            '
122            function jumpToUrl(URL) {
123                window.location.href = URL;
124                return false;
125            }
126            '
127        );
128
129        switch ($this->MOD_SETTINGS['function']) {
130            case 'search':
131                $templateFilename = 'CustomSearch.html';
132                $this->func_search();
133                break;
134            case 'records':
135                $templateFilename = 'RecordStatistics.html';
136                $this->func_records();
137                break;
138            case 'relations':
139                $templateFilename = 'Relations.html';
140                $this->func_relations();
141                break;
142            case 'refindex':
143                $templateFilename = 'ReferenceIndex.html';
144                $this->func_refindex();
145                break;
146            default:
147                $templateFilename = 'IntegrityOverview.html';
148                $this->func_default();
149        }
150        $this->view->setTemplatePathAndFilename(GeneralUtility::getFileAbsFileName($this->templatePath . $templateFilename));
151        $content = '<form action="" method="post" id="DatabaseIntegrityView" name="' . $this->formName . '">';
152        $content .= $this->view->render();
153        $content .= '</form>';
154
155        // Setting up the shortcut button for docheader
156        $buttonBar = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar();
157        // Shortcut
158        $shortCutButton = $buttonBar->makeShortcutButton()
159            ->setModuleName($this->moduleName)
160            ->setDisplayName($this->MOD_MENU['function'][$this->MOD_SETTINGS['function']])
161            ->setSetVariables(['function', 'search', 'search_query_makeQuery']);
162        $buttonBar->addButton($shortCutButton, ButtonBar::BUTTON_POSITION_RIGHT, 2);
163
164        $this->getModuleMenu();
165
166        $this->moduleTemplate->setContent($content);
167        return new HtmlResponse($this->moduleTemplate->renderContent());
168    }
169
170    /**
171     * Configure menu
172     */
173    protected function menuConfig()
174    {
175        $lang = $this->getLanguageService();
176        // MENU-ITEMS:
177        // If array, then it's a selector box menu
178        // If empty string it's just a variable, that'll be saved.
179        // Values NOT in this array will not be saved in the settings-array for the module.
180        $this->MOD_MENU = [
181            'function' => [
182                0 => htmlspecialchars($lang->getLL('menuTitle')),
183                'records' => htmlspecialchars($lang->getLL('recordStatistics')),
184                'relations' => htmlspecialchars($lang->getLL('databaseRelations')),
185                'search' => htmlspecialchars($lang->getLL('fullSearch')),
186                'refindex' => htmlspecialchars($lang->getLL('manageRefIndex'))
187            ],
188            'search' => [
189                'raw' => htmlspecialchars($lang->getLL('rawSearch')),
190                'query' => htmlspecialchars($lang->getLL('advancedQuery'))
191            ],
192            'search_query_smallparts' => '',
193            'search_result_labels' => '',
194            'labels_noprefix' => '',
195            'options_sortlabel' => '',
196            'show_deleted' => '',
197            'queryConfig' => '',
198            // Current query
199            'queryTable' => '',
200            // Current table
201            'queryFields' => '',
202            // Current tableFields
203            'queryLimit' => '',
204            // Current limit
205            'queryOrder' => '',
206            // Current Order field
207            'queryOrderDesc' => '',
208            // Current Order field descending flag
209            'queryOrder2' => '',
210            // Current Order2 field
211            'queryOrder2Desc' => '',
212            // Current Order2 field descending flag
213            'queryGroup' => '',
214            // Current Group field
215            'storeArray' => '',
216            // Used to store the available Query config memory banks
217            'storeQueryConfigs' => '',
218            // Used to store the available Query configs in memory
219            'search_query_makeQuery' => [
220                'all' => htmlspecialchars($lang->getLL('selectRecords')),
221                'count' => htmlspecialchars($lang->getLL('countResults')),
222                'explain' => htmlspecialchars($lang->getLL('explainQuery')),
223                'csv' => htmlspecialchars($lang->getLL('csvExport'))
224            ],
225            'sword' => ''
226        ];
227        // CLEAN SETTINGS
228        $OLD_MOD_SETTINGS = BackendUtility::getModuleData($this->MOD_MENU, '', $this->moduleName, 'ses');
229        $this->MOD_SETTINGS = BackendUtility::getModuleData($this->MOD_MENU, GeneralUtility::_GP('SET'), $this->moduleName, 'ses');
230        if (GeneralUtility::_GP('queryConfig')) {
231            $qA = GeneralUtility::_GP('queryConfig');
232            $this->MOD_SETTINGS = BackendUtility::getModuleData($this->MOD_MENU, ['queryConfig' => serialize($qA)], $this->moduleName, 'ses');
233        }
234        $addConditionCheck = GeneralUtility::_GP('qG_ins');
235        $setLimitToStart = false;
236        foreach ($OLD_MOD_SETTINGS as $key => $val) {
237            if (strpos($key, 'query') === 0 && $this->MOD_SETTINGS[$key] != $val && $key !== 'queryLimit' && $key !== 'use_listview') {
238                $setLimitToStart = true;
239                if ($key === 'queryTable' && !$addConditionCheck) {
240                    $this->MOD_SETTINGS['queryConfig'] = '';
241                }
242            }
243            if ($key === 'queryTable' && $this->MOD_SETTINGS[$key] != $val) {
244                $this->MOD_SETTINGS['queryFields'] = '';
245            }
246        }
247        if ($setLimitToStart) {
248            $currentLimit = explode(',', $this->MOD_SETTINGS['queryLimit']);
249            if ($currentLimit[1]) {
250                $this->MOD_SETTINGS['queryLimit'] = '0,' . $currentLimit[1];
251            } else {
252                $this->MOD_SETTINGS['queryLimit'] = '0';
253            }
254            $this->MOD_SETTINGS = BackendUtility::getModuleData($this->MOD_MENU, $this->MOD_SETTINGS, $this->moduleName, 'ses');
255        }
256    }
257
258    /**
259     * Generates the action menu
260     */
261    protected function getModuleMenu()
262    {
263        $menu = $this->moduleTemplate->getDocHeaderComponent()->getMenuRegistry()->makeMenu();
264        $menu->setIdentifier('DatabaseJumpMenu');
265        /** @var \TYPO3\CMS\Backend\Routing\UriBuilder $uriBuilder */
266        $uriBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Routing\UriBuilder::class);
267        foreach ($this->MOD_MENU['function'] as $controller => $title) {
268            $item = $menu
269                ->makeMenuItem()
270                ->setHref(
271                    (string)$uriBuilder->buildUriFromRoute(
272                        $this->moduleName,
273                        [
274                            'id' => 0,
275                            'SET' => [
276                                'function' => $controller
277                            ]
278                        ]
279                    )
280                )
281                ->setTitle($title);
282            if ($controller === $this->MOD_SETTINGS['function']) {
283                $item->setActive(true);
284            }
285            $menu->addMenuItem($item);
286        }
287        $this->moduleTemplate->getDocHeaderComponent()->getMenuRegistry()->addMenu($menu);
288    }
289
290    /**
291     * Creates the overview menu.
292     */
293    protected function func_default()
294    {
295        $modules = [];
296        $availableModFuncs = ['records', 'relations', 'search', 'refindex'];
297        /** @var \TYPO3\CMS\Backend\Routing\UriBuilder $uriBuilder */
298        $uriBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Routing\UriBuilder::class);
299        foreach ($availableModFuncs as $modFunc) {
300            $modules[$modFunc] = (string)$uriBuilder->buildUriFromRoute('system_dbint') . '&SET[function]=' . $modFunc;
301        }
302        $this->view->assign('availableFunctions', $modules);
303    }
304
305    /****************************
306     *
307     * Functionality implementation
308     *
309     ****************************/
310    /**
311     * Check and update reference index!
312     */
313    protected function func_refindex()
314    {
315        $readmeLocation = ExtensionManagementUtility::extPath('lowlevel', 'README.rst');
316        $this->view->assign('ReadmeLink', PathUtility::getAbsoluteWebPath($readmeLocation));
317        $this->view->assign('ReadmeLocation', $readmeLocation);
318        $this->view->assign('binaryPath', ExtensionManagementUtility::extPath('core', 'bin/typo3'));
319
320        if (GeneralUtility::_GP('_update') || GeneralUtility::_GP('_check')) {
321            $testOnly = (bool)GeneralUtility::_GP('_check');
322            // Call the functionality
323            $refIndexObj = GeneralUtility::makeInstance(ReferenceIndex::class);
324            $refIndexObj->enableRuntimeCache();
325            list(, $bodyContent) = $refIndexObj->updateIndex($testOnly);
326            $this->view->assign('content', str_replace('##LF##', '<br />', $bodyContent));
327        }
328    }
329
330    /**
331     * Search (Full / Advanced)
332     */
333    protected function func_search()
334    {
335        $lang = $this->getLanguageService();
336        $searchMode = $this->MOD_SETTINGS['search'];
337        $fullsearch = GeneralUtility::makeInstance(QueryView::class, $this->MOD_SETTINGS, $this->MOD_MENU, $this->moduleName);
338        $fullsearch->setFormName($this->formName);
339        $submenu = '<div class="form-inline form-inline-spaced">';
340        $submenu .= BackendUtility::getDropdownMenu(0, 'SET[search]', $searchMode, $this->MOD_MENU['search']);
341        if ($this->MOD_SETTINGS['search'] === 'query') {
342            $submenu .= BackendUtility::getDropdownMenu(0, 'SET[search_query_makeQuery]', $this->MOD_SETTINGS['search_query_makeQuery'], $this->MOD_MENU['search_query_makeQuery']) . '<br />';
343        }
344        $submenu .= '</div>';
345        if ($this->MOD_SETTINGS['search'] === 'query') {
346            $submenu .= '<div class="checkbox"><label for="checkSearch_query_smallparts">' . BackendUtility::getFuncCheck(0, 'SET[search_query_smallparts]', $this->MOD_SETTINGS['search_query_smallparts'], '', '', 'id="checkSearch_query_smallparts"') . $lang->getLL('showSQL') . '</label></div>';
347            $submenu .= '<div class="checkbox"><label for="checkSearch_result_labels">' . BackendUtility::getFuncCheck(0, 'SET[search_result_labels]', $this->MOD_SETTINGS['search_result_labels'], '', '', 'id="checkSearch_result_labels"') . $lang->getLL('useFormattedStrings') . '</label></div>';
348            $submenu .= '<div class="checkbox"><label for="checkLabels_noprefix">' . BackendUtility::getFuncCheck(0, 'SET[labels_noprefix]', $this->MOD_SETTINGS['labels_noprefix'], '', '', 'id="checkLabels_noprefix"') . $lang->getLL('dontUseOrigValues') . '</label></div>';
349            $submenu .= '<div class="checkbox"><label for="checkOptions_sortlabel">' . BackendUtility::getFuncCheck(0, 'SET[options_sortlabel]', $this->MOD_SETTINGS['options_sortlabel'], '', '', 'id="checkOptions_sortlabel"') . $lang->getLL('sortOptions') . '</label></div>';
350            $submenu .= '<div class="checkbox"><label for="checkShow_deleted">' . BackendUtility::getFuncCheck(0, 'SET[show_deleted]', $this->MOD_SETTINGS['show_deleted'], '', '', 'id="checkShow_deleted"') . $lang->getLL('showDeleted') . '</label></div>';
351        }
352        $this->view->assign('submenu', $submenu);
353        $this->view->assign('searchMode', $searchMode);
354        switch ($searchMode) {
355            case 'query':
356                $this->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Lowlevel/QueryGenerator');
357                $this->view->assign('queryMaker', $fullsearch->queryMaker());
358                break;
359            case 'raw':
360            default:
361                $this->view->assign('searchOptions', $fullsearch->form());
362                $this->view->assign('results', $fullsearch->search());
363        }
364    }
365
366    /**
367     * Records overview
368     */
369    protected function func_records()
370    {
371        /** @var DatabaseIntegrityCheck $admin */
372        $admin = GeneralUtility::makeInstance(DatabaseIntegrityCheck::class);
373        $admin->genTree(0);
374
375        // Pages stat
376        $pageStatistic = [
377            'total_pages' => [
378                'icon' => $this->iconFactory->getIconForRecord('pages', [], Icon::SIZE_SMALL)->render(),
379                'count' => count($admin->getPageIdArray())
380            ],
381            'translated_pages' => [
382                'icon' => $this->iconFactory->getIconForRecord('pages', [], Icon::SIZE_SMALL)->render(),
383                'count' => count($admin->getPageTranslatedPageIDArray()),
384            ],
385            'hidden_pages' => [
386                'icon' => $this->iconFactory->getIconForRecord('pages', ['hidden' => 1], Icon::SIZE_SMALL)->render(),
387                'count' => $admin->getRecStats()['hidden'] ?? 0
388            ],
389            'deleted_pages' => [
390                'icon' => $this->iconFactory->getIconForRecord('pages', ['deleted' => 1], Icon::SIZE_SMALL)->render(),
391                'count' => isset($admin->getRecStats()['deleted']['pages']) ? count($admin->getRecStats()['deleted']['pages']) : 0
392            ]
393        ];
394
395        $lang = $this->getLanguageService();
396
397        // Doktype
398        $doktypes = [];
399        $doktype = $GLOBALS['TCA']['pages']['columns']['doktype']['config']['items'];
400        if (is_array($doktype)) {
401            foreach ($doktype as $setup) {
402                if ($setup[1] !== '--div--') {
403                    $doktypes[] = [
404                        'icon' => $this->iconFactory->getIconForRecord('pages', ['doktype' => $setup[1]], Icon::SIZE_SMALL)->render(),
405                        'title' => $lang->sL($setup[0]) . ' (' . $setup[1] . ')',
406                        'count' => (int)($admin->getRecStats()['doktype'][$setup[1]] ?? 0)
407                    ];
408                }
409            }
410        }
411
412        // Tables and lost records
413        $id_list = '-1,0,' . implode(',', array_keys($admin->getPageIdArray()));
414        $id_list = rtrim($id_list, ',');
415        $admin->lostRecords($id_list);
416        if ($admin->fixLostRecord(GeneralUtility::_GET('fixLostRecords_table'), GeneralUtility::_GET('fixLostRecords_uid'))) {
417            $admin = GeneralUtility::makeInstance(DatabaseIntegrityCheck::class);
418            $admin->genTree(0);
419            $id_list = '-1,0,' . implode(',', array_keys($admin->getPageIdArray()));
420            $id_list = rtrim($id_list, ',');
421            $admin->lostRecords($id_list);
422        }
423        $tableStatistic = [];
424        $countArr = $admin->countRecords($id_list);
425        if (is_array($GLOBALS['TCA'])) {
426            foreach ($GLOBALS['TCA'] as $t => $value) {
427                if ($GLOBALS['TCA'][$t]['ctrl']['hideTable']) {
428                    continue;
429                }
430                if ($t === 'pages' && $admin->getLostPagesList() !== '') {
431                    $lostRecordCount = count(explode(',', $admin->getLostPagesList()));
432                } else {
433                    $lostRecordCount = isset($admin->getLRecords()[$t]) ? count($admin->getLRecords()[$t]) : 0;
434                }
435                if ($countArr['all'][$t]) {
436                    $theNumberOfRe = (int)$countArr['non_deleted'][$t] . '/' . $lostRecordCount;
437                } else {
438                    $theNumberOfRe = '';
439                }
440                $lr = '';
441                /** @var \TYPO3\CMS\Backend\Routing\UriBuilder $uriBuilder */
442                $uriBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Routing\UriBuilder::class);
443                if (is_array($admin->getLRecords()[$t])) {
444                    foreach ($admin->getLRecords()[$t] as $data) {
445                        if (!GeneralUtility::inList($admin->getLostPagesList(), $data['pid'])) {
446                            $lr .= '<div class="record"><a href="' . htmlspecialchars((string)$uriBuilder->buildUriFromRoute('system_dbint') . '&SET[function]=records&fixLostRecords_table=' . $t . '&fixLostRecords_uid=' . $data['uid']) . '" title="' . htmlspecialchars($lang->getLL('fixLostRecord')) . '">' . $this->iconFactory->getIcon('status-dialog-error', Icon::SIZE_SMALL)->render() . '</a>uid:' . $data['uid'] . ', pid:' . $data['pid'] . ', ' . htmlspecialchars(GeneralUtility::fixed_lgd_cs(strip_tags($data['title']), 20)) . '</div>';
447                        } else {
448                            $lr .= '<div class="record-noicon">uid:' . $data['uid'] . ', pid:' . $data['pid'] . ', ' . htmlspecialchars(GeneralUtility::fixed_lgd_cs(strip_tags($data['title']), 20)) . '</div>';
449                        }
450                    }
451                }
452                $tableStatistic[$t] = [
453                    'icon' => $this->iconFactory->getIconForRecord($t, [], Icon::SIZE_SMALL)->render(),
454                    'title' => $lang->sL($GLOBALS['TCA'][$t]['ctrl']['title']),
455                    'count' => $theNumberOfRe,
456                    'lostRecords' => $lr
457                ];
458            }
459        }
460
461        $this->view->assignMultiple([
462            'pages' => $pageStatistic,
463            'doktypes' => $doktypes,
464            'tables' => $tableStatistic
465        ]);
466    }
467
468    /**
469     * Show list references
470     */
471    protected function func_relations()
472    {
473        $admin = GeneralUtility::makeInstance(DatabaseIntegrityCheck::class);
474        $fkey_arrays = $admin->getGroupFields('');
475        $admin->selectNonEmptyRecordsWithFkeys($fkey_arrays);
476        $fileTest = $admin->testFileRefs();
477
478        if (is_array($fileTest['noFile'])) {
479            ksort($fileTest['noFile']);
480        }
481        $this->view->assignMultiple([
482            'files' =>  $fileTest,
483            'select_db' => $admin->testDBRefs($admin->getCheckSelectDBRefs()),
484            'group_db' => $admin->testDBRefs($admin->getCheckGroupDBRefs())
485        ]);
486    }
487
488    /**
489     * Returns the Language Service
490     * @return LanguageService
491     */
492    protected function getLanguageService()
493    {
494        return $GLOBALS['LANG'];
495    }
496
497    /**
498     * @return PageRenderer
499     */
500    protected function getPageRenderer()
501    {
502        return GeneralUtility::makeInstance(PageRenderer::class);
503    }
504}
505