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\Backend\Domain\Repository; 17 18use TYPO3\CMS\Backend\Controller\HelpController; 19use TYPO3\CMS\Backend\Module\ModuleLoader; 20use TYPO3\CMS\Core\Authentication\BackendUserAuthentication; 21use TYPO3\CMS\Core\Type\File\ImageInfo; 22use TYPO3\CMS\Core\Utility\GeneralUtility; 23use TYPO3\CMS\Core\Utility\PathUtility; 24 25/** 26 * Table manual repository for csh manual handling 27 * @internal This class is a specific Backend repository implementation and is not considered part of the Public TYPO3 API. 28 */ 29class TableManualRepository 30{ 31 /** 32 * Get the manual of the given table 33 * 34 * @param string $table 35 * @return array the manual for a TCA table, see getItem() for details 36 */ 37 public function getTableManual($table) 38 { 39 $parts = []; 40 41 // Load descriptions for table $table 42 $this->getLanguageService()->loadSingleTableDescription($table); 43 if (is_array($GLOBALS['TCA_DESCR'][$table]['columns'] ?? null) && $this->checkAccess('tables_select', $table)) { 44 // Reserved for header of table 45 $parts[0] = ''; 46 // Traverse table columns as listed in TCA_DESCR 47 foreach ($GLOBALS['TCA_DESCR'][$table]['columns'] as $field => $_) { 48 /** @var string $field */ 49 if (!$this->isExcludableField($table, $field) || $this->checkAccess('non_exclude_fields', $table . ':' . $field)) { 50 if (!$field) { 51 // Header 52 $parts[0] = $this->getItem($table, '', true); 53 } else { 54 // Field 55 $parts[] = $this->getItem($table, $field, true); 56 } 57 } 58 } 59 if (!$parts[0]) { 60 unset($parts[0]); 61 } 62 } 63 return $parts; 64 } 65 66 /** 67 * Get a single manual 68 * 69 * @param string $table table name 70 * @param string $field field name 71 * @return array 72 */ 73 public function getSingleManual($table, $field) 74 { 75 $this->getLanguageService()->loadSingleTableDescription($table); 76 return $this->getItem($table, $field); 77 } 78 79 /** 80 * Get TOC sections 81 * 82 * @param int $mode e.g. HelpController::TOC_ONLY 83 * @return array 84 */ 85 public function getSections($mode) 86 { 87 // Initialize 88 $cshKeys = array_flip(array_keys($GLOBALS['TCA_DESCR'])); 89 /** @var string[] $tcaKeys */ 90 $tcaKeys = array_keys($GLOBALS['TCA']); 91 $outputSections = []; 92 $tocArray = []; 93 // TYPO3 Core Features 94 $lang = $this->getLanguageService(); 95 $lang->loadSingleTableDescription('xMOD_csh_corebe'); 96 $this->renderTableOfContentItem($mode, 'xMOD_csh_corebe', 'core', $outputSections, $tocArray, $cshKeys); 97 // Backend Modules 98 $loadModules = GeneralUtility::makeInstance(ModuleLoader::class); 99 $loadModules->load($GLOBALS['TBE_MODULES']); 100 foreach ($loadModules->getModules() as $mainMod => $info) { 101 $cshKey = '_MOD_' . $mainMod; 102 if ($cshKeys[$cshKey] ?? '') { 103 $lang->loadSingleTableDescription($cshKey); 104 $this->renderTableOfContentItem($mode, $cshKey, 'modules', $outputSections, $tocArray, $cshKeys); 105 } 106 if (is_array($info['sub'] ?? null)) { 107 foreach ($info['sub'] as $subMod => $subInfo) { 108 $cshKey = '_MOD_' . $mainMod . '_' . $subMod; 109 if ($cshKeys[$cshKey] ?? '') { 110 $lang->loadSingleTableDescription($cshKey); 111 $this->renderTableOfContentItem($mode, $cshKey, 'modules', $outputSections, $tocArray, $cshKeys); 112 } 113 } 114 } 115 } 116 // Database Tables 117 foreach ($tcaKeys as $table) { 118 // Load descriptions for table $table 119 $lang->loadSingleTableDescription($table); 120 if (is_array($GLOBALS['TCA_DESCR'][$table]['columns'] ?? null) && $this->checkAccess('tables_select', $table)) { 121 $this->renderTableOfContentItem($mode, $table, 'tables', $outputSections, $tocArray, $cshKeys); 122 } 123 } 124 foreach ($cshKeys as $cshKey => $value) { 125 // Extensions 126 if (str_starts_with($cshKey, 'xEXT_') && !isset($GLOBALS['TCA'][$cshKey])) { 127 $lang->loadSingleTableDescription($cshKey); 128 $this->renderTableOfContentItem($mode, $cshKey, 'extensions', $outputSections, $tocArray, $cshKeys); 129 } 130 // Other 131 if (!str_starts_with($cshKey, '_MOD_') && !isset($GLOBALS['TCA'][$cshKey])) { 132 $lang->loadSingleTableDescription($cshKey); 133 $this->renderTableOfContentItem($mode, $cshKey, 'other', $outputSections, $tocArray, $cshKeys); 134 } 135 } 136 137 if ($mode === HelpController::TOC_ONLY) { 138 return $tocArray; 139 } 140 141 return [ 142 'toc' => $tocArray, 143 'content' => $outputSections, 144 ]; 145 } 146 147 /** 148 * Creates a TOC list element and renders corresponding HELP content if "renderALL" mode is set. 149 * 150 * @param int $mode Mode 151 * @param string $table CSH key / Table name 152 * @param string $tocCat TOC category keyword: "core", "modules", "tables", "other 153 * @param array $outputSections Array for accumulation of rendered HELP Content (in "renderALL" mode). Passed by reference! 154 * @param array $tocArray TOC array; Here TOC index elements are created. Passed by reference! 155 * @param array $CSHkeys CSH keys array. Every item rendered will be unset in this array so finally we can see what CSH keys are not processed yet. Passed by reference! 156 */ 157 protected function renderTableOfContentItem($mode, $table, $tocCat, &$outputSections, &$tocArray, &$CSHkeys) 158 { 159 $tocArray[$tocCat][$table] = $this->getTableFieldLabel($table); 160 if (!$mode) { 161 // Render full manual right here! 162 $outputSections[$table]['content'] = $this->getTableManual($table); 163 if (!$outputSections[$table]) { 164 unset($outputSections[$table]); 165 } 166 } 167 168 // Unset CSH key 169 unset($CSHkeys[$table]); 170 } 171 172 /** 173 * Returns composite label for table/field 174 * 175 * @param string $key CSH key / table name 176 * @param string $field Sub key / field name 177 * @param string $mergeToken Token to merge the two strings with 178 * @return string Labels joined with merge token 179 * @see getTableFieldNames() 180 */ 181 protected function getTableFieldLabel($key, $field = '', $mergeToken = ': ') 182 { 183 // Get table / field parts 184 [$tableName, $fieldName] = $this->getTableFieldNames($key, $field); 185 // Create label 186 return $this->getLanguageService()->sL($tableName) . ($field ? $mergeToken . rtrim(trim($this->getLanguageService()->sL($fieldName)), ':') : ''); 187 } 188 189 /** 190 * Returns labels for a given field in a given structure 191 * 192 * @param string $key CSH key / table name 193 * @param string $field Sub key / field name 194 * @return array Table and field labels in a numeric array 195 */ 196 protected function getTableFieldNames($key, $field) 197 { 198 $this->getLanguageService()->loadSingleTableDescription($key); 199 // Define the label for the key 200 if (!empty($GLOBALS['TCA_DESCR'][$key]['columns']['']['alttitle'])) { 201 // If there's an alternative title, use it 202 $keyName = $GLOBALS['TCA_DESCR'][$key]['columns']['']['alttitle']; 203 } elseif (isset($GLOBALS['TCA'][$key])) { 204 // Otherwise, if it's a table, use its title 205 $keyName = $GLOBALS['TCA'][$key]['ctrl']['title']; 206 } else { 207 // If no title was found, make sure to remove any "_MOD_" 208 $keyName = preg_replace('/^_MOD_/', '', $key); 209 } 210 // Define the label for the field 211 $fieldName = $field; 212 if (!empty($GLOBALS['TCA_DESCR'][$key]['columns'][$field]['alttitle'])) { 213 // If there's an alternative title, use it 214 $fieldName = $GLOBALS['TCA_DESCR'][$key]['columns'][$field]['alttitle']; 215 } elseif (!empty($GLOBALS['TCA'][$key]['columns'][$field])) { 216 // Otherwise, if it's a table, use its title 217 $fieldName = $GLOBALS['TCA'][$key]['columns'][$field]['label']; 218 } 219 return [$keyName, $fieldName]; 220 } 221 222 /** 223 * Gets a single $table/$field information piece 224 * If $anchors is set, then seeAlso references to the same table will be page-anchors, not links. 225 * 226 * @param string $table CSH key / table name 227 * @param string $field Sub key / field name 228 * @param bool $anchors If anchors is to be shown. 229 * @return array with the information 230 */ 231 protected function getItem($table, $field, $anchors = false) 232 { 233 if (!empty($table)) { 234 $field = !empty($field) ? $field : ''; 235 $setup = $GLOBALS['TCA_DESCR'][$table]['columns'][$field]; 236 return [ 237 'table' => $table, 238 'field' => $field, 239 'configuration' => $setup, 240 'headerLine' => $this->getTableFieldLabel($table, $field), 241 'content' => !empty($setup['description']) ? $setup['description'] : '', 242 'images' => !empty($setup['image']) ? $this->getImages($setup['image'], ($setup['image_descr'] ?? '')) : [], 243 'seeAlso' => !empty($setup['seeAlso']) ? $this->getSeeAlsoLinks($setup['seeAlso'], $anchors ? $table : '') : '', 244 ]; 245 } 246 return []; 247 } 248 249 /** 250 * Get see-also links 251 * 252 * @param string $value See-also input codes 253 * @param string $anchorTable If $anchorTable is set to a tablename, then references to this table will be made as anchors, not URLs. 254 * @return array See-also links 255 */ 256 protected function getSeeAlsoLinks($value, $anchorTable = '') 257 { 258 // Split references by comma or linebreak 259 $items = preg_split('/[,' . LF . ']/', $value); 260 $lines = []; 261 foreach ($items as $itemValue) { 262 $itemValue = trim($itemValue); 263 if ($itemValue) { 264 $reference = GeneralUtility::trimExplode(':', $itemValue); 265 $referenceUrl = GeneralUtility::trimExplode('|', $itemValue); 266 if (strpos(($referenceUrl[1] ?? ''), 'http') === 0) { 267 // URL reference 268 $lines[] = [ 269 'url' => $referenceUrl[1], 270 'title' => $referenceUrl[0], 271 'target' => '_blank', 272 ]; 273 } elseif (strpos(($referenceUrl[1] ?? ''), 'FILE:') === 0) { 274 // File reference 275 $fileName = GeneralUtility::getFileAbsFileName(substr($referenceUrl[1], 5)); 276 if ($fileName && @is_file($fileName)) { 277 $fileName = PathUtility::getAbsoluteWebPath($fileName); 278 $lines[] = [ 279 'url' => $fileName, 280 'title' => $referenceUrl[0], 281 'target' => '_blank', 282 ]; 283 } 284 } else { 285 // Table reference 286 $table = !empty($reference[0]) ? $reference[0] : ''; 287 $field = !empty($reference[1]) ? $reference[1] : ''; 288 $accessAllowed = true; 289 // Check if table exists and current user can access it 290 if (!empty($table)) { 291 $accessAllowed = !$this->getTableSetup($table) || $this->checkAccess('tables_select', $table); 292 } 293 // Check if field exists and is excludable or user can access it 294 if ($accessAllowed && !empty($field)) { 295 $accessAllowed = !$this->isExcludableField($table, $field) || $this->checkAccess('non_exclude_fields', $table . ':' . $field); 296 } 297 // Check read access 298 if ($accessAllowed && isset($GLOBALS['TCA_DESCR'][$table])) { 299 // Make see-also link 300 $label = $this->getTableFieldLabel($table, $field, ' / '); 301 if ($anchorTable && $table === $anchorTable) { 302 $lines[] = [ 303 'url' => '#' . rawurlencode(implode('.', $reference)), 304 'title' => $label, 305 ]; 306 } else { 307 $lines[] = [ 308 'internal' => true, 309 'arguments' => [ 310 'table' => $table, 311 'field' => $field, 312 'action' => 'detail', 313 ], 314 'title' => $label, 315 ]; 316 } 317 } 318 } 319 } 320 } 321 return $lines; 322 } 323 324 /** 325 * Check if given table / field is excludable 326 * 327 * @param string $table The table 328 * @param string $field The field 329 * @return bool TRUE if given field is excludable 330 */ 331 protected function isExcludableField($table, $field) 332 { 333 $fieldSetup = $this->getFieldSetup($table, $field); 334 if (!empty($fieldSetup)) { 335 return !empty($fieldSetup['exclude']); 336 } 337 return false; 338 } 339 340 /** 341 * Returns an array of images with description 342 * 343 * @param string $images Image file reference (list of) 344 * @param string $descriptions Description string (divided for each image by line break) 345 * @return array 346 */ 347 protected function getImages($images, $descriptions) 348 { 349 $imageData = []; 350 // Splitting 351 $imgArray = GeneralUtility::trimExplode(',', $images, true); 352 if (!empty($imgArray)) { 353 $descrArray = explode(LF, $descriptions, count($imgArray)); 354 foreach ($imgArray as $k => $image) { 355 $descriptions = $descrArray[$k] ?? ''; 356 $absImagePath = GeneralUtility::getFileAbsFileName($image); 357 if ($absImagePath && @is_file($absImagePath)) { 358 $imgFile = PathUtility::getAbsoluteWebPath($absImagePath); 359 $imageInfo = GeneralUtility::makeInstance(ImageInfo::class, $absImagePath); 360 if ($imageInfo->getWidth()) { 361 $imageData[] = [ 362 'image' => $imgFile, 363 'description' => $descriptions, 364 ]; 365 } 366 } 367 } 368 } 369 return $imageData; 370 } 371 372 /** 373 * Returns the setup for given table 374 * 375 * @param string $table The table 376 * @return array The table setup 377 */ 378 protected function getTableSetup($table) 379 { 380 if (!empty($table) && !empty($GLOBALS['TCA'][$table])) { 381 return $GLOBALS['TCA'][$table]; 382 } 383 return []; 384 } 385 386 /** 387 * Returns the setup for given table / field 388 * 389 * @param string $table The table 390 * @param string $field The field 391 * @param bool $allowEmptyField Allow empty field 392 * @return array The field setup 393 */ 394 protected function getFieldSetup($table, $field, $allowEmptyField = false) 395 { 396 $tableSetup = $this->getTableSetup($table); 397 if (!empty($tableSetup) && (!empty($field) || $allowEmptyField) && !empty($tableSetup['columns'][$field])) { 398 return $tableSetup['columns'][$field]; 399 } 400 return []; 401 } 402 403 /** 404 * Check if current backend user has access to given identifier 405 * 406 * @param string $type The type 407 * @param string $identifier The search string in access list 408 * @return bool TRUE if the user has access 409 */ 410 protected function checkAccess($type, $identifier) 411 { 412 if (!empty($type) && !empty($identifier)) { 413 return $this->getBackendUser()->check($type, $identifier); 414 } 415 return false; 416 } 417 418 /** 419 * Returns the current BE user. 420 * 421 * @return BackendUserAuthentication 422 */ 423 protected function getBackendUser(): BackendUserAuthentication 424 { 425 return $GLOBALS['BE_USER']; 426 } 427 428 /** 429 * Returns LanguageService 430 * 431 * @return \TYPO3\CMS\Core\Localization\LanguageService 432 */ 433 protected function getLanguageService() 434 { 435 return $GLOBALS['LANG']; 436 } 437} 438