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