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