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