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']) && $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->modules 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'])) {
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']) && $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 (GeneralUtility::isFirstPartOfStr($cshKey, 'xEXT_') && !isset($GLOBALS['TCA'][$cshKey])) {
127                $lang->loadSingleTableDescription($cshKey);
128                $this->renderTableOfContentItem($mode, $cshKey, 'extensions', $outputSections, $tocArray, $cshKeys);
129            }
130            // Other
131            if (!GeneralUtility::isFirstPartOfStr($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::stripPathSitePrefix($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::stripPathSitePrefix($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