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