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