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