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