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\Core\TypoScript;
17
18use TYPO3\CMS\Backend\Routing\UriBuilder;
19use TYPO3\CMS\Backend\Utility\BackendUtility;
20use TYPO3\CMS\Core\Context\Context;
21use TYPO3\CMS\Core\Database\ConnectionPool;
22use TYPO3\CMS\Core\Database\Query\QueryBuilder;
23use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
24use TYPO3\CMS\Core\Database\Query\Restriction\WorkspaceRestriction;
25use TYPO3\CMS\Core\Exception;
26use TYPO3\CMS\Core\Imaging\Icon;
27use TYPO3\CMS\Core\Imaging\IconFactory;
28use TYPO3\CMS\Core\Localization\LanguageService;
29use TYPO3\CMS\Core\Page\JavaScriptModuleInstruction;
30use TYPO3\CMS\Core\TypoScript\Parser\ConstantConfigurationParser;
31use TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser;
32use TYPO3\CMS\Core\Utility\ArrayUtility;
33use TYPO3\CMS\Core\Utility\GeneralUtility;
34use TYPO3\CMS\Core\Utility\MathUtility;
35use TYPO3\CMS\Frontend\Configuration\TypoScript\ConditionMatching\ConditionMatcher;
36
37/**
38 * TSParser extension class to TemplateService
39 * Contains functions for the TS module in TYPO3 backend
40 *
41 * @internal this is only used for the TYPO3 TypoScript Template module, which should not be used in Extensions
42 */
43class ExtendedTemplateService extends TemplateService
44{
45    /**
46     * @var array
47     */
48    protected $categories = [
49        'basic' => [],
50        // Constants of superior importance for the template-layout. This is dimensions, imagefiles and enabling of various features. The most basic constants, which you would almost always want to configure.
51        'menu' => [],
52        // Menu setup. This includes fontfiles, sizes, background images. Depending on the menutype.
53        'content' => [],
54        // All constants related to the display of pagecontent elements
55        'page' => [],
56        // General configuration like metatags, link targets
57        'advanced' => [],
58        // Advanced functions, which are used very seldom.
59        'all' => [],
60    ];
61
62    /**
63     * Tsconstanteditor
64     *
65     * @var int
66     */
67    public $ext_inBrace = 0;
68
69    /**
70     * Tsbrowser
71     *
72     * @var array
73     */
74    public $tsbrowser_searchKeys = [];
75
76    /**
77     * @var array
78     */
79    public $tsbrowser_depthKeys = [];
80
81    /**
82     * @var string
83     */
84    public $constantMode = '';
85
86    /**
87     * @var string
88     */
89    public $regexMode = '';
90
91    /**
92     * @var int
93     */
94    public $ext_expandAllNotes = 0;
95
96    /**
97     * @var int
98     */
99    public $ext_noPMicons = 0;
100
101    /**
102     * Ts analyzer
103     *
104     * @var array
105     */
106    public $templateTitles = [];
107
108    /**
109     * @var array|null
110     */
111    protected $lnToScript;
112
113    /**
114     * @var array
115     */
116    public $clearList_const_temp;
117
118    /**
119     * @var array
120     */
121    public $clearList_setup_temp;
122
123    /**
124     * @var string
125     */
126    public $bType = '';
127
128    /**
129     * @var bool
130     */
131    public $linkObjects = false;
132
133    /**
134     * @var bool
135     */
136    public $changed = false;
137
138    /**
139     * @var int[]
140     */
141    protected $objReg = [];
142
143    /**
144     * @var array
145     */
146    public $raw = [];
147
148    /**
149     * @var int
150     */
151    public $rawP = 0;
152
153    /**
154     * @var string
155     */
156    public $lastComment = '';
157
158    /**
159     * @var array<string, JavaScriptModuleInstruction>
160     */
161    protected $javaScriptInstructions = [];
162
163    /**
164     * @var \TYPO3\CMS\Core\TypoScript\Parser\ConstantConfigurationParser
165     */
166    private $constantParser;
167
168    /**
169     * @param Context|null $context
170     * @param \TYPO3\CMS\Core\TypoScript\Parser\ConstantConfigurationParser $constantParser
171     */
172    public function __construct(Context $context = null, ConstantConfigurationParser $constantParser = null)
173    {
174        parent::__construct($context);
175        $this->constantParser = $constantParser ?? GeneralUtility::makeInstance(ConstantConfigurationParser::class);
176        // Disabled in backend context
177        $this->tt_track = false;
178        $this->verbose = false;
179    }
180
181    /**
182     * @return array<string, JavaScriptModuleInstruction>
183     */
184    public function getJavaScriptInstructions(): array
185    {
186        return $this->javaScriptInstructions;
187    }
188
189    /**
190     * Substitute constant
191     *
192     * @param string $all
193     * @return string
194     */
195    public function substituteConstants($all)
196    {
197        return preg_replace_callback('/\\{\\$(.[^}]+)\\}/', [$this, 'substituteConstantsCallBack'], $all);
198    }
199
200    /**
201     * Call back method for preg_replace_callback in substituteConstants
202     *
203     * @param array $matches Regular expression matches
204     * @return string Replacement
205     * @see substituteConstants()
206     */
207    public function substituteConstantsCallBack($matches)
208    {
209        $marker = substr(md5($matches[0]), 0, 6);
210        switch ($this->constantMode) {
211            case 'const':
212                $ret_val = isset($this->flatSetup[$matches[1]]) && !is_array($this->flatSetup[$matches[1]]) ? '##' . $marker . '_B##' . $this->flatSetup[$matches[1]] . '##' . $marker . '_M##' . $matches[0] . '##' . $marker . '_E##' : $matches[0];
213                break;
214            case 'subst':
215                $ret_val = isset($this->flatSetup[$matches[1]]) && !is_array($this->flatSetup[$matches[1]]) ? '##' . $marker . '_B##' . $matches[0] . '##' . $marker . '_M##' . $this->flatSetup[$matches[1]] . '##' . $marker . '_E##' : $matches[0];
216                break;
217            case 'untouched':
218                $ret_val = $matches[0];
219                break;
220            default:
221                $ret_val = isset($this->flatSetup[$matches[1]]) && !is_array($this->flatSetup[$matches[1]]) ? $this->flatSetup[$matches[1]] : $matches[0];
222        }
223        return $ret_val;
224    }
225
226    /**
227     * Substitute markers added in substituteConstantsCallBack()
228     * with ##6chars_B##value1##6chars_M##value2##6chars_E##
229     *
230     * @param string $all
231     * @return string
232     */
233    public function substituteCMarkers($all)
234    {
235        switch ($this->constantMode) {
236            case 'const':
237            case 'subst':
238                $all = preg_replace(
239                    '/##[a-z0-9]{6}_B##(.*?)##[a-z0-9]{6}_M##(.*?)##[a-z0-9]{6}_E##/',
240                    '<strong class="text-success" data-bs-toggle="tooltip" data-bs-placement="top" data-title="$1" title="$1">$2</strong>',
241                    $all
242                );
243                break;
244            default:
245        }
246        return $all;
247    }
248
249    /**
250     * Parse constants with respect to the constant-editor in this module.
251     * In particular comments in the code are registered and the edit_divider is taken into account.
252     *
253     * @return array
254     */
255    public function generateConfig_constants()
256    {
257        // Parse constants
258        $constants = GeneralUtility::makeInstance(TypoScriptParser::class);
259        // Register comments!
260        $constants->regComments = true;
261        /** @var ConditionMatcher $matchObj */
262        $matchObj = GeneralUtility::makeInstance(ConditionMatcher::class);
263        // Matches ALL conditions in TypoScript
264        $matchObj->setSimulateMatchResult(true);
265        $c = 0;
266        $cc = count($this->constants);
267        $defaultConstants = [];
268        foreach ($this->constants as $str) {
269            $c++;
270            if ($c == $cc) {
271                $defaultConstants = ArrayUtility::flatten($constants->setup, '', true);
272            }
273            $constants->parse($str, $matchObj);
274        }
275        $this->setup['constants'] = $constants->setup;
276        $flatSetup = ArrayUtility::flatten($constants->setup, '', true);
277        return $this->constantParser->parseComments(
278            $flatSetup,
279            $defaultConstants
280        );
281    }
282
283    /**
284     * @param array $theSetup
285     * @param string $theKey
286     * @return array{0: array, 1: string}
287     */
288    public function ext_getSetup($theSetup, $theKey)
289    {
290        $theKey = trim((string)$theKey);
291        if (empty($theKey)) {
292            // Early return the whole setup in case key is empty
293            return [(array)$theSetup, ''];
294        }
295        // 'a.b.c' --> ['a', 'b.c']
296        $parts = explode('.', $theKey, 2);
297        $pathSegment = $parts[0] ?? '';
298        $pathRest = trim($parts[1] ?? '');
299        if ($pathSegment !== '' && is_array($theSetup[$pathSegment . '.'] ?? false)) {
300            if ($pathRest !== '') {
301                // Current path segment is a sub array, check it recursively by applying the rest of the key
302                return $this->ext_getSetup($theSetup[$pathSegment . '.'], $pathRest);
303            }
304            // No further path to evaluate, return current setup and the value for the current path segment - if any
305            return [$theSetup[$pathSegment . '.'], $theSetup[$pathSegment] ?? ''];
306        }
307        // Return the key value - if any - along with an empty setup since no sub array exists
308        return [[], $theSetup[$theKey] ?? ''];
309    }
310
311    /**
312     * Get object tree
313     *
314     * @param array $arr
315     * @param string $depth_in
316     * @param string $depthData
317     * @param string $parentType (unused)
318     * @param string $parentValue (unused)
319     * @param string $alphaSort sorts the array keys / tree by alphabet when set to 1
320     * @return string
321     */
322    public function ext_getObjTree($arr, $depth_in, $depthData, $parentType = '', $parentValue = '', $alphaSort = '0')
323    {
324        $HTML = '';
325        if ($alphaSort == '1') {
326            ksort($arr);
327        }
328        $keyArr_num = [];
329        $keyArr_alpha = [];
330        /** @var \TYPO3\CMS\Backend\Routing\UriBuilder $uriBuilder */
331        $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
332        foreach ($arr as $key => $value) {
333            // Don't do anything with comments / linenumber registrations...
334            if (substr($key, -2) !== '..') {
335                $key = preg_replace('/\\.$/', '', $key) ?? '';
336                if (substr($key, -1) !== '.') {
337                    if (MathUtility::canBeInterpretedAsInteger($key)) {
338                        $keyArr_num[$key] = $arr[$key] ?? '';
339                    } else {
340                        $keyArr_alpha[$key] = $arr[$key] ?? '';
341                    }
342                }
343            }
344        }
345        ksort($keyArr_num);
346        $keyArr = $keyArr_num + $keyArr_alpha;
347        if ($depth_in) {
348            $depth_in = $depth_in . '.';
349        }
350        foreach ($keyArr as $key => $value) {
351            $depth = $depth_in . $key;
352            // This excludes all constants starting with '_' from being shown.
353            if ($this->bType !== 'const' || $depth[0] !== '_') {
354                $goto = substr(md5($depth), 0, 6);
355                $deeper = is_array($arr[$key . '.'] ?? null) && (($this->tsbrowser_depthKeys[$depth] ?? false) || $this->ext_expandAllNotes);
356                $PM = is_array($arr[$key . '.'] ?? null) && !$this->ext_noPMicons ? ($deeper ? 'minus' : 'plus') : 'join';
357                $HTML .= $depthData . '<li><span class="list-tree-group">';
358                if ($PM !== 'join') {
359                    $urlParameters = [
360                        'id' => (int)GeneralUtility::_GP('id'),
361                        'tsbr[' . $depth . ']' => $deeper ? 0 : 1,
362                    ];
363                    $aHref = (string)$uriBuilder->buildUriFromRoute('web_ts', $urlParameters) . '#' . $goto;
364                    $HTML .= '<a class="list-tree-control' . ($PM === 'minus' ? ' list-tree-control-open' : ' list-tree-control-closed') . '" name="' . $goto . '" href="' . htmlspecialchars($aHref) . '"><i class="fa"></i></a>';
365                }
366                $label = $key;
367                // Read only...
368                if (($depth === 'types') && $this->bType === 'setup') {
369                    $label = '<span style="color: #666666;">' . $label . '</span>';
370                } else {
371                    if ($this->linkObjects) {
372                        $urlParameters = [
373                            'id' => (int)GeneralUtility::_GP('id'),
374                            'sObj' => $depth,
375                        ];
376                        $aHref = (string)$uriBuilder->buildUriFromRoute('web_ts', $urlParameters);
377                        if ($this->bType !== 'const') {
378                            $ln = is_array($arr[$key . '.ln..'] ?? null) ? 'Defined in: ' . $this->lineNumberToScript($arr[$key . '.ln..']) : 'N/A';
379                        } else {
380                            $ln = '';
381                        }
382                        if (($this->tsbrowser_searchKeys[$depth] ?? 0) & 4) {
383                            // The key has matched the search string
384                            $label = '<strong class="text-danger">' . $label . '</strong>';
385                        }
386                        $label = '<a href="' . htmlspecialchars($aHref) . '" title="' . htmlspecialchars($depth_in . $key . ' ' . $ln) . '">' . $label . '</a>';
387                    }
388                }
389                $HTML .= '<span class="list-tree-label" title="' . htmlspecialchars($depth_in . $key) . '">[' . $label . ']</span>';
390                if (isset($arr[$key])) {
391                    $theValue = $arr[$key];
392                    // The value has matched the search string
393                    if (($this->tsbrowser_searchKeys[$depth] ?? 0) & 2) {
394                        $HTML .= ' = <span class="list-tree-value text-danger">' . htmlspecialchars($theValue) . '</span>';
395                    } else {
396                        $HTML .= ' = <span class="list-tree-value">' . htmlspecialchars($theValue) . '</span>';
397                    }
398                    if ($this->ext_regComments && isset($arr[$key . '..'])) {
399                        $comment = (string)$arr[$key . '..'];
400                        // Skip INCLUDE_TYPOSCRIPT comments, they are almost useless
401                        if (!preg_match('/### <INCLUDE_TYPOSCRIPT:.*/', $comment)) {
402                            // Remove linebreaks, replace with ' '
403                            $comment = preg_replace('/[\\r\\n]/', ' ', $comment) ?? '';
404                            // Remove # and * if more than twice in a row
405                            $comment = preg_replace('/[#\\*]{2,}/', '', $comment) ?? '';
406                            // Replace leading # (just if it exists) and add it again. Result: Every comment should be prefixed by a '#'.
407                            $comment = preg_replace('/^[#\\*\\s]+/', '# ', $comment) ?? '';
408                            // Masking HTML Tags: Replace < with &lt; and > with &gt;
409                            $comment = htmlspecialchars($comment);
410                            $HTML .= ' <i class="text-muted">' . trim($comment) . '</i>';
411                        }
412                    }
413                }
414                $HTML .= '</span>';
415                if ($deeper) {
416                    $HTML .= $this->ext_getObjTree($arr[$key . '.'] ?? [], $depth, $depthData, '', $arr[$key] ?? '', $alphaSort);
417                }
418            }
419        }
420        if ($HTML !== '') {
421            $HTML = '<ul class="list-tree text-monospace">' . $HTML . '</ul>';
422        }
423
424        return $HTML;
425    }
426
427    /**
428     * Find the originating template name for an array of line numbers (TypoScript setup only!)
429     * Given an array of linenumbers the method will try to find the corresponding template where this line originated
430     * The linenumber indicates the *last* lineNumber that is part of the template
431     *
432     * lineNumbers are in sync with the calculated lineNumbers '.ln..' in TypoScriptParser
433     *
434     * @param array $lnArr Array with linenumbers (might have some extra symbols, for example for unsetting) to be processed
435     * @return string Imploded array of line number and template title
436     */
437    public function lineNumberToScript(array $lnArr)
438    {
439        // On the first call, construct the lnToScript array.
440        if (!is_array($this->lnToScript)) {
441            $this->lnToScript = [];
442
443            // aggregatedTotalLineCount
444            $c = 0;
445            foreach ($this->hierarchyInfo as $templateNumber => $info) {
446                // hierarchyInfo has the number of lines in configLines, but unfortunately this value
447                // was calculated *before* processing of any INCLUDE instructions
448                // for some yet unknown reason we have to add an extra +2 offset
449                $linecountAfterIncludeProcessing = substr_count($this->config[$templateNumber], LF) + 2;
450                $c += $linecountAfterIncludeProcessing;
451                $this->lnToScript[$c] = $info['title'];
452            }
453        }
454
455        foreach ($lnArr as $k => $ln) {
456            foreach ($this->lnToScript as $endLn => $title) {
457                if ($endLn >= (int)$ln) {
458                    $lnArr[$k] = '"' . $title . '", ' . $ln;
459                    break;
460                }
461            }
462        }
463
464        return implode('; ', $lnArr);
465    }
466
467    /**
468     * @param array $arr
469     * @param string $depth_in
470     * @param string $searchString
471     * @param array $keyArray
472     * @return array
473     * @throws Exception
474     */
475    public function ext_getSearchKeys($arr, $depth_in, $searchString, $keyArray)
476    {
477        $keyArr = [];
478        foreach ($arr as $key => $value) {
479            $key = preg_replace('/\\.$/', '', $key) ?? '';
480            if (substr($key, -1) !== '.') {
481                $keyArr[$key] = 1;
482            }
483        }
484        if ($depth_in) {
485            $depth_in = $depth_in . '.';
486        }
487        $searchPattern = '';
488        if ($this->regexMode) {
489            $searchPattern = '/' . addcslashes($searchString, '/') . '/';
490            $matchResult = @preg_match($searchPattern, '');
491            if ($matchResult === false) {
492                throw new Exception(sprintf('Error evaluating regular expression "%s".', $searchPattern), 1446559458);
493            }
494        }
495        foreach ($keyArr as $key => $value) {
496            $depth = $depth_in . $key;
497            if ($this->regexMode) {
498                // The value has matched
499                if (($arr[$key] ?? false) && preg_match($searchPattern, $arr[$key])) {
500                    $this->tsbrowser_searchKeys[$depth] = ($this->tsbrowser_searchKeys[$depth] ?? 0) + 2;
501                }
502                // The key has matched
503                if (preg_match($searchPattern, $key)) {
504                    $this->tsbrowser_searchKeys[$depth] = ($this->tsbrowser_searchKeys[$depth] ?? 0) + 4;
505                }
506                // Just open this subtree if the parent key has matched the search
507                if (preg_match($searchPattern, $depth_in)) {
508                    $this->tsbrowser_searchKeys[$depth] = 1;
509                }
510            } else {
511                // The value has matched
512                if (($arr[$key] ?? false) && stripos($arr[$key], $searchString) !== false) {
513                    $this->tsbrowser_searchKeys[$depth] = ($this->tsbrowser_searchKeys[$depth] ?? 0) + 2;
514                }
515                // The key has matches
516                if (stripos($key, $searchString) !== false) {
517                    $this->tsbrowser_searchKeys[$depth] = ($this->tsbrowser_searchKeys[$depth] ?? 0) + 4;
518                }
519                // Just open this subtree if the parent key has matched the search
520                if (stripos($depth_in, $searchString) !== false) {
521                    $this->tsbrowser_searchKeys[$depth] = 1;
522                }
523            }
524            if (is_array($arr[$key . '.'] ?? null)) {
525                $cS = count($this->tsbrowser_searchKeys);
526                $keyArray = $this->ext_getSearchKeys($arr[$key . '.'], $depth, $searchString, $keyArray);
527                if ($cS !== count($this->tsbrowser_searchKeys)) {
528                    $keyArray[$depth] = 1;
529                }
530            }
531        }
532        return $keyArray;
533    }
534
535    /**
536     * @param int $pid
537     * @return int
538     */
539    public function ext_getRootlineNumber($pid)
540    {
541        if ($pid) {
542            foreach ($this->getRootLine() as $key => $val) {
543                if ((int)$val['uid'] === (int)$pid) {
544                    return (int)$key;
545                }
546            }
547        }
548        return -1;
549    }
550
551    /**
552     * @param array $arr
553     * @param string $depthData
554     * @param array $keyArray
555     * @param int $first
556     * @return array
557     */
558    public function ext_getTemplateHierarchyArr($arr, $depthData, $keyArray, $first = 0)
559    {
560        $keyArr = [];
561        foreach ($arr as $key => $value) {
562            $key = preg_replace('/\\.$/', '', $key) ?? '';
563            if (substr($key, -1) !== '.') {
564                $keyArr[$key] = 1;
565            }
566        }
567        $a = 0;
568        $c = count($keyArr);
569        /** @var \TYPO3\CMS\Backend\Routing\UriBuilder $uriBuilder */
570        $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
571        /** @var IconFactory $iconFactory */
572        $iconFactory = GeneralUtility::makeInstance(IconFactory::class);
573        foreach ($keyArr as $key => $value) {
574            $HTML = '';
575            $a++;
576            $deeper = is_array($arr[$key . '.'] ?? false);
577            $row = $arr[$key];
578            $LN = $a == $c ? 'blank' : 'line';
579            $BTM = $a == $c ? 'top' : '';
580            $HTML .= $depthData;
581            $alttext = '[' . $row['templateID'] . ']';
582            $alttext .= $row['pid'] ? ' - ' . BackendUtility::getRecordPath($row['pid'], '1=1', 20) : '';
583            $icon = strpos($row['templateID'], 'sys') === 0
584                ? '<span title="' . htmlspecialchars($alttext) . '">' . $iconFactory->getIconForRecord('sys_template', $row, Icon::SIZE_SMALL)->render() . '</span>'
585                : '<span title="' . htmlspecialchars($alttext) . '">' . $iconFactory->getIcon('mimetypes-x-content-template-static', Icon::SIZE_SMALL)->render() . '</span>';
586            if (in_array($row['templateID'], $this->clearList_const) || in_array($row['templateID'], $this->clearList_setup)) {
587                $urlParameters = [
588                    'id' => (int)GeneralUtility::_GP('id'),
589                    'template' => $row['templateID'],
590                ];
591                $aHref = (string)$uriBuilder->buildUriFromRoute('web_ts', $urlParameters);
592                $A_B = '<a href="' . htmlspecialchars($aHref) . '">';
593                $A_E = '</a>';
594                if (GeneralUtility::_GP('template') == $row['templateID']) {
595                    $A_B = '<strong>' . $A_B;
596                    $A_E .= '</strong>';
597                }
598            } else {
599                $A_B = '';
600                $A_E = '';
601            }
602            $HTML .= ($first ? '' : '<span class="treeline-icon treeline-icon-join' . $BTM . '"></span>') . $icon . ' ' . $A_B
603                . htmlspecialchars(GeneralUtility::fixed_lgd_cs($row['title'], $GLOBALS['BE_USER']->uc['titleLen']))
604                . $A_E . '&nbsp;&nbsp;';
605            $RL = $this->ext_getRootlineNumber($row['pid']);
606            $statusCheckedIcon = $iconFactory->getIcon('status-status-checked', Icon::SIZE_SMALL)->render();
607            $keyArray[] = '<tr>
608							<td class="nowrap">' . $HTML . '</td>
609							<td align="center">' . ($row['root'] ? $statusCheckedIcon : '') . '</td>
610							<td align="center">' . ($row['clConf'] ? $statusCheckedIcon : '') . '</td>
611							<td align="center">' . ($row['clConst'] ? $statusCheckedIcon : '') . '</td>
612							<td align="center">' . ($row['pid'] ?: '') . '</td>
613							<td align="center">' . ($RL >= 0 ? $RL : '') . '</td>
614						</tr>';
615            if ($deeper) {
616                $keyArray = $this->ext_getTemplateHierarchyArr($arr[$key . '.'], $depthData . ($first ? '' : '<span class="treeline-icon treeline-icon-' . $LN . '"></span>'), $keyArray);
617            }
618        }
619        return $keyArray;
620    }
621
622    /**
623     * Processes the flat array from TemplateService->hierarchyInfo
624     * and turns it into a hierarchical array to show dependencies (used by TemplateAnalyzer)
625     *
626     * @param array $depthDataArr (empty array on external call)
627     * @param int $pointer Element number (1! to count()) of $this->hierarchyInfo that should be processed.
628     * @return array Processed hierachyInfo.
629     */
630    public function ext_process_hierarchyInfo(array $depthDataArr, &$pointer)
631    {
632        $parent = $this->hierarchyInfo[$pointer - 1]['templateParent'];
633        while ($pointer > 0 && $this->hierarchyInfo[$pointer - 1]['templateParent'] == $parent) {
634            $pointer--;
635            $row = $this->hierarchyInfo[$pointer];
636            $depthDataArr[$row['templateID']] = $row;
637            unset($this->clearList_setup_temp[$row['templateID']]);
638            unset($this->clearList_const_temp[$row['templateID']]);
639            $this->templateTitles[$row['templateID']] = $row['title'];
640            if ($row['templateID'] == ($this->hierarchyInfo[$pointer - 1]['templateParent'] ?? '')) {
641                $depthDataArr[$row['templateID'] . '.'] = $this->ext_process_hierarchyInfo([], $pointer);
642            }
643        }
644        return $depthDataArr;
645    }
646
647    /**
648     * Get a single sys_template record attached to a single page.
649     * If multiple template records are on this page, the first (order by sorting)
650     * record will be returned, unless a specific template uid is specified via $templateUid
651     *
652     * @param int $pid The pid to select sys_template records from
653     * @param int $templateUid Optional template uid
654     * @return array|null Returns the template record or null if none was found
655     */
656    public function ext_getFirstTemplate($pid, $templateUid = 0)
657    {
658        if (empty($pid)) {
659            return null;
660        }
661
662        // Query is taken from the runThroughTemplates($theRootLine) function in the parent class.
663        $queryBuilder = $this->getTemplateQueryBuilder($pid)
664            ->setMaxResults(1);
665        if ($templateUid) {
666            $queryBuilder->andWhere(
667                $queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($templateUid, \PDO::PARAM_INT))
668            );
669        }
670        $row = $queryBuilder->executeQuery()->fetchAssociative();
671        BackendUtility::workspaceOL('sys_template', $row);
672
673        return $row;
674    }
675
676    /**
677     * Get an array of all template records on a page.
678     *
679     * @param int $pid Pid to fetch sys_template records for
680     * @return array[] Array of template records
681     */
682    public function ext_getAllTemplates($pid): array
683    {
684        if (empty($pid)) {
685            return [];
686        }
687        $result = $this->getTemplateQueryBuilder($pid)->executeQuery();
688        $outRes = [];
689        while ($row = $result->fetchAssociative()) {
690            BackendUtility::workspaceOL('sys_template', $row);
691            if (is_array($row)) {
692                $outRes[] = $row;
693            }
694        }
695        return $outRes;
696    }
697
698    /**
699     * Internal helper method to prepare the query builder for
700     * getting sys_template records from a given pid
701     *
702     * @param int $pid The pid to select sys_template records from
703     * @return QueryBuilder Returns a QueryBuilder
704     */
705    protected function getTemplateQueryBuilder(int $pid): QueryBuilder
706    {
707        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
708            ->getQueryBuilderForTable('sys_template');
709        $queryBuilder->getRestrictions()
710            ->removeAll()
711            ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
712            ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $GLOBALS['BE_USER']->workspace));
713
714        $queryBuilder->select('*')
715            ->from('sys_template')
716            ->where(
717                $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pid, \PDO::PARAM_INT))
718            );
719        if (!empty($GLOBALS['TCA']['sys_template']['ctrl']['sortby'])) {
720            $queryBuilder->orderBy($GLOBALS['TCA']['sys_template']['ctrl']['sortby']);
721        }
722
723        return $queryBuilder;
724    }
725
726    /**
727     * @param array $editConstArray
728     */
729    public function ext_categorizeEditableConstants($editConstArray)
730    {
731        // Runs through the available constants and fills the $this->categories array with pointers and priority-info
732        foreach ($editConstArray as $constName => $constData) {
733            if (!$constData['type']) {
734                $constData['type'] = 'string';
735            }
736            $cats = explode(',', $constData['cat']);
737            // if = only one category, while allows for many. We have agreed on only one category is the most basic way...
738            foreach ($cats as $theCat) {
739                $theCat = trim($theCat);
740                if ($theCat) {
741                    $this->categories[$theCat][$constName] = $constData['subcat'];
742                }
743            }
744        }
745    }
746
747    /**
748     * @return array
749     */
750    public function ext_getCategoryLabelArray()
751    {
752        // Returns array used for labels in the menu.
753        $retArr = [];
754        foreach ($this->categories as $k => $v) {
755            if (!empty($v)) {
756                $retArr[$k] = strtoupper($k) . ' (' . count($v) . ')';
757            }
758        }
759        return $retArr;
760    }
761
762    /**
763     * @param string $type
764     * @return array
765     */
766    public function ext_getTypeData($type)
767    {
768        $retArr = [];
769        $type = trim($type);
770        if (!$type) {
771            $retArr['type'] = 'string';
772        } else {
773            $m = strcspn($type, ' [');
774            $retArr['type'] = strtolower(substr($type, 0, $m));
775            $types = ['int' => 1, 'options' => 1, 'file' => 1, 'boolean' => 1, 'offset' => 1, 'user' => 1];
776            if (isset($types[$retArr['type']])) {
777                $p = trim(substr($type, $m));
778                $reg = [];
779                preg_match('/\\[(.*)\\]/', $p, $reg);
780                $p = trim($reg[1] ?? '');
781                if ($p) {
782                    $retArr['paramstr'] = $p;
783                    switch ($retArr['type']) {
784                        case 'int':
785                            if ($retArr['paramstr'][0] === '-') {
786                                $retArr['params'] = GeneralUtility::intExplode('-', substr($retArr['paramstr'], 1));
787                                $retArr['params'][0] = (int)('-' . $retArr['params'][0]);
788                            } else {
789                                $retArr['params'] = GeneralUtility::intExplode('-', $retArr['paramstr']);
790                            }
791                            $retArr['min'] = $retArr['params'][0];
792                            $retArr['max'] = $retArr['params'][1];
793                            $retArr['paramstr'] = $retArr['params'][0] . ' - ' . $retArr['params'][1];
794                            break;
795                        case 'options':
796                            $retArr['params'] = explode(',', $retArr['paramstr']);
797                            break;
798                    }
799                }
800            }
801        }
802        return $retArr;
803    }
804
805    /**
806     * @param array $params
807     * @return array
808     */
809    public function ext_fNandV($params)
810    {
811        $fN = 'data[' . $params['name'] . ']';
812        $idName = str_replace('.', '-', $params['name']);
813        $fV = $params['value'];
814        // Values entered from the constantsedit cannot be constants!	230502; removed \{ and set {
815        if (preg_match('/^{[\\$][a-zA-Z0-9\\.]*}$/', trim($fV), $reg)) {
816            $fV = '';
817        }
818        $fV = htmlspecialchars($fV);
819        return [$fN, $fV, $params, $idName];
820    }
821
822    /**
823     * This functions returns the HTML-code that creates the editor-layout of the module.
824     *
825     * @param array $theConstants
826     * @param string $category
827     * @return array
828     */
829    public function ext_printFields($theConstants, $category): array
830    {
831        reset($theConstants);
832        $groupedOutput = [];
833        $subcat = '';
834        if (!empty($this->categories[$category]) && is_array($this->categories[$category])) {
835            asort($this->categories[$category]);
836            /** @var IconFactory $iconFactory */
837            $iconFactory = GeneralUtility::makeInstance(IconFactory::class);
838            $categoryLoop = 0;
839            foreach ($this->categories[$category] as $name => $type) {
840                $params = $theConstants[$name];
841                if (is_array($params)) {
842                    if ($subcat !== (string)($params['subcat_name'] ?? '')) {
843                        $categoryLoop++;
844                        $subcat = (string)($params['subcat_name'] ?? '');
845                        $subcat_name = $subcat ? (string)($this->constantParser->getSubCategories()[$subcat][0] ?? '') : 'Others';
846                        $groupedOutput[$categoryLoop] = [
847                            'label' => $subcat_name,
848                            'fields' => [],
849                        ];
850                    }
851                    $label = $this->getLanguageService()->sL($params['label']);
852                    $label_parts = explode(':', $label, 2);
853                    if (count($label_parts) === 2) {
854                        $head = trim($label_parts[0]);
855                        $body = trim($label_parts[1]);
856                    } else {
857                        $head = trim($label_parts[0]);
858                        $body = '';
859                    }
860                    $typeDat = $this->ext_getTypeData($params['type']);
861                    $p_field = '';
862                    $fragmentName = substr(md5($params['name']), 0, 10);
863                    $fragmentNameEscaped = htmlspecialchars($fragmentName);
864                    [$fN, $fV, $params, $idName] = $this->ext_fNandV($params);
865                    $idName = htmlspecialchars($idName);
866                    $hint = '';
867                    switch ($typeDat['type']) {
868                        case 'int':
869                        case 'int+':
870                            $additionalAttributes = '';
871                            if ($typeDat['paramstr'] ?? false) {
872                                $hint = ' Range: ' . $typeDat['paramstr'];
873                            } elseif ($typeDat['type'] === 'int+') {
874                                $hint = ' Range: 0 - ';
875                                $typeDat['min'] = 0;
876                            } else {
877                                $hint = ' (Integer)';
878                            }
879
880                            if (isset($typeDat['min'])) {
881                                $additionalAttributes .= ' min="' . (int)$typeDat['min'] . '" ';
882                            }
883                            if (isset($typeDat['max'])) {
884                                $additionalAttributes .= ' max="' . (int)$typeDat['max'] . '" ';
885                            }
886
887                            $p_field =
888                                '<input class="form-control" id="' . $idName . '" type="number"'
889                                . ' name="' . $fN . '" value="' . $fV . '" data-form-update-fragment="' . $fragmentNameEscaped . '" ' . $additionalAttributes . ' />';
890                            break;
891                        case 'color':
892                            $p_field = '
893                                <input class="form-control formengine-colorpickerelement t3js-color-picker" type="text" id="input-' . $idName . '" rel="' . $idName .
894                                '" name="' . $fN . '" value="' . $fV . '" data-form-update-fragment="' . $fragmentNameEscaped . '"/>';
895
896                            $this->javaScriptInstructions['color'] ??= JavaScriptModuleInstruction::forRequireJS('TYPO3/CMS/Backend/ColorPicker')
897                                ->invoke('initialize');
898                            break;
899                        case 'wrap':
900                            $wArr = explode('|', $fV);
901                            $p_field = '<div class="input-group">
902                                            <input class="form-control form-control-adapt" type="text" id="' . $idName . '" name="' . $fN . '" value="' . $wArr[0] . '" data-form-update-fragment="' . $fragmentNameEscaped . '" />
903                                            <span class="input-group-addon input-group-icon">|</span>
904                                            <input class="form-control form-control-adapt" type="text" name="W' . $fN . '" value="' . $wArr[1] . '" data-form-update-fragment="' . $fragmentNameEscaped . '" />
905                                         </div>';
906                            break;
907                        case 'offset':
908                            $wArr = explode(',', $fV);
909                            $labels = GeneralUtility::trimExplode(',', $typeDat['paramstr']);
910                            $p_field = '<span class="input-group-addon input-group-icon">' . ($labels[0] ?: 'x') . '</span><input type="text" class="form-control form-control-adapt" name="' . $fN . '" value="' . $wArr[0] . '" data-form-update-fragment="' . $fragmentNameEscaped . '" />';
911                            $p_field .= '<span class="input-group-addon input-group-icon">' . ($labels[1] ?: 'y') . '</span><input type="text" name="W' . $fN . '" value="' . $wArr[1] . '" class="form-control form-control-adapt" data-form-update-fragment="' . $fragmentNameEscaped . '" />';
912                            $labelsCount = count($labels);
913                            for ($aa = 2; $aa < $labelsCount; $aa++) {
914                                if ($labels[$aa]) {
915                                    $p_field .= '<span class="input-group-addon input-group-icon">' . $labels[$aa] . '</span><input type="text" name="W' . $aa . $fN . '" value="' . $wArr[$aa] . '" class="form-control form-control-adapt" data-form-update-fragment="' . $fragmentNameEscaped . '" />';
916                                } else {
917                                    $p_field .= '<input type="hidden" name="W' . $aa . $fN . '" value="' . $wArr[$aa] . '" />';
918                                }
919                            }
920                            $p_field = '<div class="input-group">' . $p_field . '</div>';
921                            break;
922                        case 'options':
923                            if (is_array($typeDat['params'])) {
924                                $p_field = '';
925                                foreach ($typeDat['params'] as $val) {
926                                    $vParts = explode('=', $val, 2);
927                                    $label = $vParts[0];
928                                    $val = $vParts[1] ?? $vParts[0];
929                                    // option tag:
930                                    $sel = '';
931                                    if ($val === $params['value']) {
932                                        $sel = ' selected';
933                                    }
934                                    $p_field .= '<option value="' . htmlspecialchars($val) . '"' . $sel . '>' . $this->getLanguageService()->sL($label) . '</option>';
935                                }
936                                $p_field = '<select class="form-select" id="' . $idName . '" name="' . $fN . '" data-form-update-fragment="' . $fragmentNameEscaped . '">' . $p_field . '</select>';
937                            }
938                            break;
939                        case 'boolean':
940                            $sel = $fV ? 'checked' : '';
941                            $p_field =
942                                '<input type="hidden" name="' . $fN . '" value="0" />'
943                                . '<label class="btn btn-default btn-checkbox">'
944                                . '<input id="' . $idName . '" type="checkbox" name="' . $fN . '" value="' . (($typeDat['paramstr'] ?? false) ?: 1) . '" ' . $sel . ' data-form-update-fragment="' . $fragmentNameEscaped . '" />'
945                                . '<span class="t3-icon fa"></span>'
946                                . '</label>';
947                            break;
948                        case 'comment':
949                            $sel = $fV ? '' : 'checked';
950                            $p_field =
951                                '<input type="hidden" name="' . $fN . '" value="" />'
952                                . '<label class="btn btn-default btn-checkbox">'
953                                . '<input id="' . $idName . '" type="checkbox" name="' . $fN . '" value="1" ' . $sel . ' data-form-update-fragment="' . $fragmentNameEscaped . '" />'
954                                . '<span class="t3-icon fa"></span>'
955                                . '</label>';
956                            break;
957                        case 'file':
958                            // extensionlist
959                            $extList = $typeDat['paramstr'];
960                            if ($extList === 'IMAGE_EXT') {
961                                $extList = $GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext'];
962                            }
963                            $p_field = '<option value="">(' . $extList . ')</option>';
964                            if (trim($params['value'])) {
965                                $val = $params['value'];
966                                $p_field .= '<option value=""></option>';
967                                $p_field .= '<option value="' . htmlspecialchars($val) . '" selected>' . $val . '</option>';
968                            }
969                            $p_field = '<select class="form-select" id="' . $idName . '" name="' . $fN . '" data-form-update-fragment="' . $fragmentNameEscaped . '">' . $p_field . '</select>';
970                            break;
971                        case 'user':
972                            $userFunction = $typeDat['paramstr'];
973                            $userFunctionParams = ['fieldName' => $fN, 'fieldValue' => $fV];
974                            $p_field = GeneralUtility::callUserFunction($userFunction, $userFunctionParams, $this);
975                            break;
976                        default:
977                            $p_field = '<input class="form-control" id="' . $idName . '" type="text" name="' . $fN . '" value="' . $fV . '" data-form-update-fragment="' . $fragmentNameEscaped . '" />';
978                    }
979                    // Define default names and IDs
980                    $userTyposcriptID = 'userTS-' . $idName;
981                    $defaultTyposcriptID = 'defaultTS-' . $idName;
982                    $userTyposcriptStyle = '';
983                    // Set the default styling options
984                    if (isset($this->objReg[$params['name']])) {
985                        $checkboxValue = 'checked';
986                        $defaultTyposcriptStyle = 'style="display:none;"';
987                    } else {
988                        $checkboxValue = '';
989                        $userTyposcriptStyle = 'style="display:none;"';
990                        $defaultTyposcriptStyle = '';
991                    }
992                    $deleteIconHTML =
993                        '<button type="button" class="btn btn-default t3js-toggle" data-bs-toggle="undo" rel="' . $idName . '">'
994                            . '<span title="' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.deleteTitle')) . '">'
995                                . $iconFactory->getIcon('actions-edit-undo', Icon::SIZE_SMALL)->render()
996                            . '</span>'
997                        . '</button>';
998                    $editIconHTML =
999                        '<button type="button" class="btn btn-default t3js-toggle" data-bs-toggle="edit"  rel="' . $idName . '">'
1000                            . '<span title="' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.editTitle')) . '">'
1001                                . $iconFactory->getIcon('actions-open', Icon::SIZE_SMALL)->render()
1002                            . '</span>'
1003                        . '</button>';
1004                    $constantCheckbox = '<input type="hidden" name="check[' . $params['name'] . ']" id="check-' . $idName . '" value="' . $checkboxValue . '"/>';
1005                    // If there's no default value for the field, use a static label.
1006                    if (!$params['default_value']) {
1007                        $params['default_value'] = '[Empty]';
1008                    }
1009                    $constantDefaultRow =
1010                        '<div class="input-group defaultTS" id="' . $defaultTyposcriptID . '" ' . $defaultTyposcriptStyle . '>'
1011                            . '<span class="input-group-btn">' . $editIconHTML . '</span>'
1012                            . '<input class="form-control" type="text" placeholder="' . htmlspecialchars($params['default_value']) . '" readonly>'
1013                        . '</div>';
1014                    $constantEditRow =
1015                        '<div class="input-group userTS" id="' . $userTyposcriptID . '" ' . $userTyposcriptStyle . '>'
1016                            . '<span class="input-group-btn">' . $deleteIconHTML . '</span>'
1017                            . $p_field
1018                        . '</div>';
1019                    $constantData =
1020                        $constantCheckbox
1021                        . $constantEditRow
1022                        . $constantDefaultRow;
1023
1024                    $groupedOutput[$categoryLoop]['items'][] = [
1025                        'identifier' => $fragmentName,
1026                        'label' => $head,
1027                        'name' => $params['name'],
1028                        'description' => $body,
1029                        'hint' => $hint,
1030                        'data' => $constantData,
1031                    ];
1032                } else {
1033                    debug('Error. Constant did not exist. Should not happen.');
1034                }
1035            }
1036        }
1037        return $groupedOutput;
1038    }
1039
1040    /***************************
1041     *
1042     * Processing input values
1043     *
1044     ***************************/
1045    /**
1046     * @param string $constants
1047     */
1048    public function ext_regObjectPositions(string $constants): void
1049    {
1050        // This runs through the lines of the constants-field of the active template and registers the constants-names
1051        // and line positions in an array, $this->objReg
1052        $this->raw = explode(LF, $constants);
1053        $this->rawP = 0;
1054        // Resetting the objReg if the divider is found!!
1055        $this->objReg = [];
1056        $this->ext_regObjects('');
1057    }
1058
1059    /**
1060     * @param string $pre
1061     */
1062    public function ext_regObjects($pre)
1063    {
1064        // Works with regObjectPositions. "expands" the names of the TypoScript objects
1065        while (isset($this->raw[$this->rawP])) {
1066            $line = ltrim($this->raw[$this->rawP]);
1067            $this->rawP++;
1068            if ($line) {
1069                if ($line[0] === '[') {
1070                } elseif (strcspn($line, '}#/') != 0) {
1071                    $varL = strcspn($line, ' {=<');
1072                    $var = substr($line, 0, $varL);
1073                    $line = ltrim(substr($line, $varL));
1074                    switch ($line[0]) {
1075                        case '=':
1076                            $this->objReg[$pre . $var] = $this->rawP - 1;
1077                            break;
1078                        case '{':
1079                            $this->ext_inBrace++;
1080                            $this->ext_regObjects($pre . $var . '.');
1081                            break;
1082                    }
1083                    $this->lastComment = '';
1084                } elseif ($line[0] === '}') {
1085                    $this->lastComment = '';
1086                    $this->ext_inBrace--;
1087                    if ($this->ext_inBrace < 0) {
1088                        $this->ext_inBrace = 0;
1089                    } else {
1090                        break;
1091                    }
1092                }
1093            }
1094        }
1095    }
1096
1097    /**
1098     * @param string $key
1099     * @param string $var
1100     */
1101    public function ext_putValueInConf($key, $var)
1102    {
1103        // Puts the value $var to the TypoScript value $key in the current lines of the templates.
1104        // If the $key is not found in the template constants field, a new line is inserted in the bottom.
1105        $theValue = ' ' . trim($var);
1106        if (isset($this->objReg[$key])) {
1107            $lineNum = $this->objReg[$key];
1108            $parts = explode('=', $this->raw[$lineNum], 2);
1109            if (count($parts) === 2) {
1110                $parts[1] = $theValue;
1111            }
1112            $this->raw[$lineNum] = implode('=', $parts);
1113        } else {
1114            $this->raw[] = $key . ' =' . $theValue;
1115        }
1116        $this->changed = true;
1117    }
1118
1119    /**
1120     * @param string $key
1121     */
1122    public function ext_removeValueInConf($key)
1123    {
1124        // Removes the value in the configuration
1125        if (isset($this->objReg[$key])) {
1126            $lineNum = $this->objReg[$key];
1127            unset($this->raw[$lineNum]);
1128        }
1129        $this->changed = true;
1130    }
1131
1132    /**
1133     * @param array $arr
1134     * @param array $settings
1135     * @return array
1136     */
1137    public function ext_depthKeys($arr, $settings)
1138    {
1139        $tsbrArray = [];
1140        foreach ($arr as $theK => $theV) {
1141            $theKeyParts = explode('.', $theK);
1142            $depth = '';
1143            $c = count($theKeyParts);
1144            $a = 0;
1145            foreach ($theKeyParts as $p) {
1146                $a++;
1147                $depth .= ($depth ? '.' : '') . $p;
1148                $tsbrArray[$depth] = $c == $a ? $theV : 1;
1149            }
1150        }
1151        // Modify settings
1152        foreach ($tsbrArray as $theK => $theV) {
1153            if ($theV) {
1154                $settings[$theK] = 1;
1155            } else {
1156                unset($settings[$theK]);
1157            }
1158        }
1159        return $settings;
1160    }
1161
1162    /**
1163     * Process input
1164     *
1165     * @param array $http_post_vars
1166     * @param array $http_post_files (not used anymore)
1167     * @param array $theConstants
1168     * @param array $tplRow Not used
1169     */
1170    public function ext_procesInput($http_post_vars, $http_post_files, $theConstants, $tplRow)
1171    {
1172        $data = $http_post_vars['data'] ?? null;
1173        $check = $http_post_vars['check'] ?? [];
1174        $Wdata = $http_post_vars['Wdata'] ?? [];
1175        $W2data = $http_post_vars['W2data'] ?? [];
1176        $W3data = $http_post_vars['W3data'] ?? [];
1177        $W4data = $http_post_vars['W4data'] ?? [];
1178        $W5data = $http_post_vars['W5data'] ?? [];
1179        if (is_array($data)) {
1180            foreach ($data as $key => $var) {
1181                if (isset($theConstants[$key])) {
1182                    // If checkbox is set, update the value
1183                    if (isset($check[$key])) {
1184                        // Exploding with linebreak, just to make sure that no multiline input is given!
1185                        [$var] = explode(LF, $var);
1186                        $typeDat = $this->ext_getTypeData($theConstants[$key]['type']);
1187                        switch ($typeDat['type']) {
1188                            case 'int':
1189                                if ($typeDat['paramstr'] ?? false) {
1190                                    $var = MathUtility::forceIntegerInRange((int)$var, $typeDat['params'][0], $typeDat['params'][1]);
1191                                } else {
1192                                    $var = (int)$var;
1193                                }
1194                                break;
1195                            case 'int+':
1196                                $var = max(0, (int)$var);
1197                                break;
1198                            case 'color':
1199                                $col = [];
1200                                if ($var) {
1201                                    $var = preg_replace('/[^A-Fa-f0-9]*/', '', $var) ?? '';
1202                                    $useFulHex = strlen($var) > 3;
1203                                    $col[] = (int)hexdec($var[0]);
1204                                    $col[] = (int)hexdec($var[1]);
1205                                    $col[] = (int)hexdec($var[2]);
1206                                    if ($useFulHex) {
1207                                        $col[] = (int)hexdec($var[3]);
1208                                        $col[] = (int)hexdec($var[4]);
1209                                        $col[] = (int)hexdec($var[5]);
1210                                    }
1211                                    $var = substr('0' . dechex($col[0]), -1) . substr('0' . dechex($col[1]), -1) . substr('0' . dechex($col[2]), -1);
1212                                    if ($useFulHex) {
1213                                        $var .= substr('0' . dechex($col[3]), -1) . substr('0' . dechex($col[4]), -1) . substr('0' . dechex($col[5]), -1);
1214                                    }
1215                                    $var = '#' . strtoupper($var);
1216                                }
1217                                break;
1218                            case 'comment':
1219                                if ($var) {
1220                                    $var = '';
1221                                } else {
1222                                    $var = '#';
1223                                }
1224                                break;
1225                            case 'wrap':
1226                                if (isset($Wdata[$key])) {
1227                                    $var .= '|' . $Wdata[$key];
1228                                }
1229                                break;
1230                            case 'offset':
1231                                if (isset($Wdata[$key])) {
1232                                    $var = (int)$var . ',' . (int)$Wdata[$key];
1233                                    if (isset($W2data[$key])) {
1234                                        $var .= ',' . (int)$W2data[$key];
1235                                        if (isset($W3data[$key])) {
1236                                            $var .= ',' . (int)$W3data[$key];
1237                                            if (isset($W4data[$key])) {
1238                                                $var .= ',' . (int)$W4data[$key];
1239                                                if (isset($W5data[$key])) {
1240                                                    $var .= ',' . (int)$W5data[$key];
1241                                                }
1242                                            }
1243                                        }
1244                                    }
1245                                }
1246                                break;
1247                            case 'boolean':
1248                                if ($var) {
1249                                    $var = ($typeDat['paramstr'] ?? false) ?: 1;
1250                                }
1251                                break;
1252                        }
1253                        if ((string)($theConstants[$key]['value'] ?? '') !== (string)$var) {
1254                            // Put value in, if changed.
1255                            $this->ext_putValueInConf($key, $var);
1256                        }
1257                        // Remove the entry because it has been "used"
1258                        unset($check[$key]);
1259                    } else {
1260                        $this->ext_removeValueInConf($key);
1261                    }
1262                }
1263            }
1264        }
1265        // Remaining keys in $check indicates fields that are just clicked "on" to be edited.
1266        // Therefore we get the default value and puts that in the template as a start...
1267        foreach ($check ?? [] as $key => $var) {
1268            if (isset($theConstants[$key])) {
1269                $dValue = $theConstants[$key]['default_value'];
1270                $this->ext_putValueInConf($key, $dValue);
1271            }
1272        }
1273    }
1274
1275    /**
1276     * @param int $id
1277     * @param string $perms_clause
1278     * @return array
1279     */
1280    public function ext_prevPageWithTemplate($id, $perms_clause)
1281    {
1282        $rootLine = BackendUtility::BEgetRootLine($id, $perms_clause ? ' AND ' . $perms_clause : '');
1283        foreach ($rootLine as $p) {
1284            if ($this->ext_getFirstTemplate($p['uid'])) {
1285                return $p;
1286            }
1287        }
1288        return [];
1289    }
1290
1291    /**
1292     * Is set by runThroughTemplates(), previously set via TemplateAnalyzerModuleFunctionController from the outside
1293     *
1294     * @return array
1295     */
1296    protected function getRootLine()
1297    {
1298        return is_array($this->absoluteRootLine) ? $this->absoluteRootLine : [];
1299    }
1300
1301    /**
1302     * @return LanguageService
1303     */
1304    protected function getLanguageService()
1305    {
1306        return $GLOBALS['LANG'];
1307    }
1308}
1309