1<?php
2namespace TYPO3\CMS\Core\TypoScript;
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\Template\DocumentTemplate;
18use TYPO3\CMS\Backend\Utility\BackendUtility;
19use TYPO3\CMS\Core\Context\Context;
20use TYPO3\CMS\Core\Database\ConnectionPool;
21use TYPO3\CMS\Core\Database\Query\QueryBuilder;
22use TYPO3\CMS\Core\Database\Query\Restriction\BackendWorkspaceRestriction;
23use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
24use TYPO3\CMS\Core\Exception;
25use TYPO3\CMS\Core\Imaging\Icon;
26use TYPO3\CMS\Core\Imaging\IconFactory;
27use TYPO3\CMS\Core\Localization\LanguageService;
28use TYPO3\CMS\Core\Utility\GeneralUtility;
29use TYPO3\CMS\Core\Utility\MathUtility;
30use TYPO3\CMS\Frontend\Configuration\TypoScript\ConditionMatching\ConditionMatcher;
31
32/**
33 * TSParser extension class to TemplateService
34 * Contains functions for the TS module in TYPO3 backend
35 *
36 * @internal this is only used for the TYPO3 TypoScript Template module, which should not be used in Extensions
37 */
38class ExtendedTemplateService extends TemplateService
39{
40    /**
41     * @var array
42     */
43    public $categories = [
44        'basic' => [],
45        // 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.
46        'menu' => [],
47        // Menu setup. This includes fontfiles, sizes, background images. Depending on the menutype.
48        'content' => [],
49        // All constants related to the display of pagecontent elements
50        'page' => [],
51        // General configuration like metatags, link targets
52        'advanced' => [],
53        // Advanced functions, which are used very seldom.
54        'all' => []
55    ];
56
57    /**
58     * Translated categories
59     *
60     * @var array
61     */
62    protected $categoryLabels = [];
63
64    /**
65     * This will be filled with the available categories of the current template.
66     *
67     * @var array
68     */
69    public $subCategories = [
70        // Standard categories:
71        'enable' => ['Enable features', 'a'],
72        'dims' => ['Dimensions, widths, heights, pixels', 'b'],
73        'file' => ['Files', 'c'],
74        'typo' => ['Typography', 'd'],
75        'color' => ['Colors', 'e'],
76        'links' => ['Links and targets', 'f'],
77        'language' => ['Language specific constants', 'g'],
78        // subcategories based on the default content elements
79        'cheader' => ['Content: \'Header\'', 'ma'],
80        'cheader_g' => ['Content: \'Header\', Graphical', 'ma'],
81        'ctext' => ['Content: \'Text\'', 'mb'],
82        'cimage' => ['Content: \'Image\'', 'md'],
83        'ctextmedia' => ['Content: \'Textmedia\'', 'ml'],
84        'cbullets' => ['Content: \'Bullet list\'', 'me'],
85        'ctable' => ['Content: \'Table\'', 'mf'],
86        'cuploads' => ['Content: \'Filelinks\'', 'mg'],
87        'cmultimedia' => ['Content: \'Multimedia\'', 'mh'],
88        'cmedia' => ['Content: \'Media\'', 'mr'],
89        'cmailform' => ['Content: \'Form\'', 'mi'],
90        'csearch' => ['Content: \'Search\'', 'mj'],
91        'clogin' => ['Content: \'Login\'', 'mk'],
92        'cmenu' => ['Content: \'Menu/Sitemap\'', 'mm'],
93        'cshortcut' => ['Content: \'Insert records\'', 'mn'],
94        'clist' => ['Content: \'List of records\'', 'mo'],
95        'chtml' => ['Content: \'HTML\'', 'mq']
96    ];
97
98    /**
99     * Tsconstanteditor
100     *
101     * @var int
102     */
103    public $ext_inBrace = 0;
104
105    /**
106     * Tsbrowser
107     *
108     * @var array
109     */
110    public $tsbrowser_searchKeys = [];
111
112    /**
113     * @var array
114     */
115    public $tsbrowser_depthKeys = [];
116
117    /**
118     * @var string
119     */
120    public $constantMode = '';
121
122    /**
123     * @var string
124     */
125    public $regexMode = '';
126
127    /**
128     * @var string
129     */
130    public $fixedLgd = '';
131
132    /**
133     * @var int
134     */
135    public $ext_lineNumberOffset = 0;
136
137    /**
138     * @var int
139     */
140    public $ext_expandAllNotes = 0;
141
142    /**
143     * @var int
144     */
145    public $ext_noPMicons = 0;
146
147    /**
148     * @var array
149     */
150    public $ext_listOfTemplatesArr = [];
151
152    /**
153     * @var string
154     */
155    public $ext_lineNumberOffset_mode = '';
156
157    /**
158     * Don't change
159     *
160     * @var int
161     */
162    public $ext_dontCheckIssetValues = 0;
163
164    /**
165     * @var int
166     */
167    public $ext_printAll = 0;
168
169    /**
170     * @var string
171     */
172    public $ext_CEformName = 'forms[0]';
173
174    /**
175     * @var bool
176     */
177    public $doNotSortCategoriesBeforeMakingForm = false;
178
179    /**
180     * Ts analyzer
181     *
182     * @var array
183     */
184    public $templateTitles = [];
185
186    /**
187     * @var array|null
188     */
189    protected $lnToScript;
190
191    /**
192     * @var array
193     */
194    public $clearList_const_temp;
195
196    /**
197     * @var array
198     */
199    public $clearList_setup_temp;
200
201    /**
202     * @var string
203     */
204    public $bType = '';
205
206    /**
207     * @var bool
208     */
209    public $linkObjects = false;
210
211    /**
212     * @var bool
213     */
214    public $changed = false;
215
216    /**
217     * @var int[]
218     */
219    protected $objReg = [];
220
221    /**
222     * @var array
223     */
224    public $raw = [];
225
226    /**
227     * @var int
228     */
229    public $rawP = 0;
230
231    /**
232     * @var string
233     */
234    public $lastComment = '';
235
236    /**
237     * @var array
238     */
239    protected $inlineJavaScript = [];
240
241    /**
242     * @param Context|null $context
243     */
244    public function __construct(Context $context = null)
245    {
246        parent::__construct($context);
247        // Disabled in backend context
248        $this->tt_track = false;
249        $this->verbose = false;
250    }
251
252    /**
253     * Gets the inline JavaScript.
254     *
255     * @return array
256     */
257    public function getInlineJavaScript()
258    {
259        return $this->inlineJavaScript;
260    }
261
262    /**
263     * Substitute constant
264     *
265     * @param string $all
266     * @return string
267     */
268    public function substituteConstants($all)
269    {
270        return preg_replace_callback('/\\{\\$(.[^}]+)\\}/', [$this, 'substituteConstantsCallBack'], $all);
271    }
272
273    /**
274     * Call back method for preg_replace_callback in substituteConstants
275     *
276     * @param array $matches Regular expression matches
277     * @return string Replacement
278     * @see substituteConstants()
279     */
280    public function substituteConstantsCallBack($matches)
281    {
282        $marker = substr(md5($matches[0]), 0, 6);
283        switch ($this->constantMode) {
284            case 'const':
285                $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];
286                break;
287            case 'subst':
288                $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];
289                break;
290            case 'untouched':
291                $ret_val = $matches[0];
292                break;
293            default:
294                $ret_val = isset($this->flatSetup[$matches[1]]) && !is_array($this->flatSetup[$matches[1]]) ? $this->flatSetup[$matches[1]] : $matches[0];
295        }
296        return $ret_val;
297    }
298
299    /**
300     * Substitute markers added in substituteConstantsCallBack()
301     * with ##6chars_B##value1##6chars_M##value2##6chars_E##
302     *
303     * @param string $all
304     * @return string
305     */
306    public function substituteCMarkers($all)
307    {
308        switch ($this->constantMode) {
309            case 'const':
310            case 'subst':
311                $all = preg_replace(
312                    '/##[a-z0-9]{6}_B##(.*?)##[a-z0-9]{6}_M##(.*?)##[a-z0-9]{6}_E##/',
313                    '<strong class="text-success" data-toggle="tooltip" data-placement="top" data-title="$1" title="$1">$2</strong>',
314                    $all
315                );
316                break;
317            default:
318        }
319        return $all;
320    }
321
322    /**
323     * Parse constants with respect to the constant-editor in this module.
324     * In particular comments in the code are registered and the edit_divider is taken into account.
325     *
326     * @return array
327     */
328    public function generateConfig_constants()
329    {
330        // These vars are also set lateron...
331        $this->setup['sitetitle'] = $this->sitetitle;
332        // Parse constants
333        $constants = GeneralUtility::makeInstance(Parser\TypoScriptParser::class);
334        // Register comments!
335        $constants->regComments = true;
336        $constants->setup = $this->mergeConstantsFromPageTSconfig([]);
337        /** @var ConditionMatcher $matchObj */
338        $matchObj = GeneralUtility::makeInstance(ConditionMatcher::class);
339        // Matches ALL conditions in TypoScript
340        $matchObj->setSimulateMatchResult(true);
341        $c = 0;
342        $cc = count($this->constants);
343        $defaultConstants = [];
344        foreach ($this->constants as $str) {
345            $c++;
346            if ($c == $cc) {
347                $this->flatSetup = [];
348                $this->flattenSetup($constants->setup, '');
349                $defaultConstants = $this->flatSetup;
350            }
351            $constants->parse($str, $matchObj);
352        }
353        $this->flatSetup = [];
354        $this->flattenSetup($constants->setup, '');
355        $this->setup['constants'] = $constants->setup;
356        return $this->ext_compareFlatSetups($defaultConstants);
357    }
358
359    /**
360     * @param array $theSetup
361     * @param string $theKey
362     * @return array
363     */
364    public function ext_getSetup($theSetup, $theKey)
365    {
366        $parts = explode('.', $theKey, 2);
367        if ((string)$parts[0] !== '' && is_array($theSetup[$parts[0] . '.'])) {
368            if (trim($parts[1]) !== '') {
369                return $this->ext_getSetup($theSetup[$parts[0] . '.'], trim($parts[1]));
370            }
371            return [$theSetup[$parts[0] . '.'], $theSetup[$parts[0]]];
372        }
373        if (trim($theKey) !== '') {
374            return [[], $theSetup[$theKey]];
375        }
376        return [$theSetup, ''];
377    }
378
379    /**
380     * Get object tree
381     *
382     * @param array $arr
383     * @param string $depth_in
384     * @param string $depthData
385     * @param string $parentType (unused)
386     * @param string $parentValue (unused)
387     * @param string $alphaSort sorts the array keys / tree by alphabet when set to 1
388     * @return string
389     */
390    public function ext_getObjTree($arr, $depth_in, $depthData, $parentType = '', $parentValue = '', $alphaSort = '0')
391    {
392        $HTML = '';
393        if ($alphaSort == '1') {
394            ksort($arr);
395        }
396        $keyArr_num = [];
397        $keyArr_alpha = [];
398        /** @var \TYPO3\CMS\Backend\Routing\UriBuilder $uriBuilder */
399        $uriBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Routing\UriBuilder::class);
400        foreach ($arr as $key => $value) {
401            // Don't do anything with comments / linenumber registrations...
402            if (substr($key, -2) !== '..') {
403                $key = preg_replace('/\\.$/', '', $key);
404                if (substr($key, -1) !== '.') {
405                    if (MathUtility::canBeInterpretedAsInteger($key)) {
406                        $keyArr_num[$key] = $arr[$key];
407                    } else {
408                        $keyArr_alpha[$key] = $arr[$key];
409                    }
410                }
411            }
412        }
413        ksort($keyArr_num);
414        $keyArr = $keyArr_num + $keyArr_alpha;
415        if ($depth_in) {
416            $depth_in = $depth_in . '.';
417        }
418        foreach ($keyArr as $key => $value) {
419            $depth = $depth_in . $key;
420            // This excludes all constants starting with '_' from being shown.
421            if ($this->bType !== 'const' || $depth[0] !== '_') {
422                $goto = substr(md5($depth), 0, 6);
423                $deeper = is_array($arr[$key . '.']) && ($this->tsbrowser_depthKeys[$depth] || $this->ext_expandAllNotes);
424                $PM = is_array($arr[$key . '.']) && !$this->ext_noPMicons ? ($deeper ? 'minus' : 'plus') : 'join';
425                $HTML .= $depthData . '<li><span class="list-tree-group">';
426                if ($PM !== 'join') {
427                    $urlParameters = [
428                        'id' => (int)GeneralUtility::_GP('id'),
429                        'tsbr[' . $depth . ']' => $deeper ? 0 : 1
430                    ];
431                    if (GeneralUtility::_GP('breakPointLN')) {
432                        $urlParameters['breakPointLN'] = GeneralUtility::_GP('breakPointLN');
433                    }
434                    $aHref = (string)$uriBuilder->buildUriFromRoute('web_ts', $urlParameters) . '#' . $goto;
435                    $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>';
436                }
437                $label = $key;
438                // Read only...
439                if (($depth === 'types' || $depth === 'resources' || $depth === 'sitetitle') && $this->bType === 'setup') {
440                    $label = '<span style="color: #666666;">' . $label . '</span>';
441                } else {
442                    if ($this->linkObjects) {
443                        $urlParameters = [
444                            'id' => (int)GeneralUtility::_GP('id'),
445                            'sObj' => $depth
446                        ];
447                        if (GeneralUtility::_GP('breakPointLN')) {
448                            $urlParameters['breakPointLN'] = GeneralUtility::_GP('breakPointLN');
449                        }
450                        $aHref = (string)$uriBuilder->buildUriFromRoute('web_ts', $urlParameters);
451                        if ($this->bType !== 'const') {
452                            $ln = is_array($arr[$key . '.ln..']) ? 'Defined in: ' . $this->lineNumberToScript($arr[$key . '.ln..']) : 'N/A';
453                        } else {
454                            $ln = '';
455                        }
456                        if ($this->tsbrowser_searchKeys[$depth] & 4) {
457                            // The key has matched the search string
458                            $label = '<strong class="text-danger">' . $label . '</strong>';
459                        }
460                        $label = '<a href="' . htmlspecialchars($aHref) . '" title="' . htmlspecialchars($depth_in . $key . ' ' . $ln) . '">' . $label . '</a>';
461                    }
462                }
463                $HTML .= '<span class="list-tree-label" title="' . htmlspecialchars($depth_in . $key) . '">[' . $label . ']</span>';
464                if (isset($arr[$key])) {
465                    $theValue = $arr[$key];
466                    if ($this->fixedLgd) {
467                        $imgBlocks = ceil(1 + strlen($depthData) / 77);
468                        $lgdChars = 68 - ceil(strlen('[' . $key . ']') * 0.8) - $imgBlocks * 3;
469                        $theValue = $this->ext_fixed_lgd($theValue, $lgdChars);
470                    }
471                    // The value has matched the search string
472                    if ($this->tsbrowser_searchKeys[$depth] & 2) {
473                        $HTML .= ' = <span class="list-tree-value text-danger">' . htmlspecialchars($theValue) . '</span>';
474                    } else {
475                        $HTML .= ' = <span class="list-tree-value">' . htmlspecialchars($theValue) . '</span>';
476                    }
477                    if ($this->ext_regComments && isset($arr[$key . '..'])) {
478                        $comment = $arr[$key . '..'];
479                        // Skip INCLUDE_TYPOSCRIPT comments, they are almost useless
480                        if (!preg_match('/### <INCLUDE_TYPOSCRIPT:.*/', $comment)) {
481                            // Remove linebreaks, replace with ' '
482                            $comment = preg_replace('/[\\r\\n]/', ' ', $comment);
483                            // Remove # and * if more than twice in a row
484                            $comment = preg_replace('/[#\\*]{2,}/', '', $comment);
485                            // Replace leading # (just if it exists) and add it again. Result: Every comment should be prefixed by a '#'.
486                            $comment = preg_replace('/^[#\\*\\s]+/', '# ', $comment);
487                            // Masking HTML Tags: Replace < with &lt; and > with &gt;
488                            $comment = htmlspecialchars($comment);
489                            $HTML .= ' <i class="text-muted">' . trim($comment) . '</i>';
490                        }
491                    }
492                }
493                $HTML .= '</span>';
494                if ($deeper) {
495                    $HTML .= $this->ext_getObjTree($arr[$key . '.'], $depth, $depthData, '', $arr[$key], $alphaSort);
496                }
497            }
498        }
499        if ($HTML !== '') {
500            $HTML = '<ul class="list-tree text-monospace">' . $HTML . '</ul>';
501        }
502
503        return $HTML;
504    }
505
506    /**
507     * Find the originating template name for an array of line numbers (TypoScript setup only!)
508     * Given an array of linenumbers the method will try to find the corresponding template where this line originated
509     * The linenumber indicates the *last* lineNumber that is part of the template
510     *
511     * lineNumbers are in sync with the calculated lineNumbers '.ln..' in TypoScriptParser
512     *
513     * @param array $lnArr Array with linenumbers (might have some extra symbols, for example for unsetting) to be processed
514     * @return array The same array where each entry has been prepended by the template title if available
515     */
516    public function lineNumberToScript(array $lnArr)
517    {
518        // On the first call, construct the lnToScript array.
519        if (!is_array($this->lnToScript)) {
520            $this->lnToScript = [];
521
522            // aggregatedTotalLineCount
523            $c = 0;
524            foreach ($this->hierarchyInfo as $templateNumber => $info) {
525                // hierarchyInfo has the number of lines in configLines, but unfortunately this value
526                // was calculated *before* processing of any INCLUDE instructions
527                // for some yet unknown reason we have to add an extra +2 offset
528                $linecountAfterIncludeProcessing = substr_count($this->config[$templateNumber], LF) + 2;
529                $c += $linecountAfterIncludeProcessing;
530                $this->lnToScript[$c] = $info['title'];
531            }
532        }
533
534        foreach ($lnArr as $k => $ln) {
535            foreach ($this->lnToScript as $endLn => $title) {
536                if ($endLn >= (int)$ln) {
537                    $lnArr[$k] = '"' . $title . '", ' . $ln;
538                    break;
539                }
540            }
541        }
542
543        return implode('; ', $lnArr);
544    }
545
546    /**
547     * @param array $arr
548     * @param string $depth_in
549     * @param string $searchString
550     * @param array $keyArray
551     * @return array
552     * @throws Exception
553     */
554    public function ext_getSearchKeys($arr, $depth_in, $searchString, $keyArray)
555    {
556        $keyArr = [];
557        foreach ($arr as $key => $value) {
558            $key = preg_replace('/\\.$/', '', $key);
559            if (substr($key, -1) !== '.') {
560                $keyArr[$key] = 1;
561            }
562        }
563        if ($depth_in) {
564            $depth_in = $depth_in . '.';
565        }
566        $searchPattern = '';
567        if ($this->regexMode) {
568            $searchPattern = '/' . addcslashes($searchString, '/') . '/';
569            $matchResult = @preg_match($searchPattern, '');
570            if ($matchResult === false) {
571                throw new Exception(sprintf('Error evaluating regular expression "%s".', $searchPattern), 1446559458);
572            }
573        }
574        foreach ($keyArr as $key => $value) {
575            $depth = $depth_in . $key;
576            $deeper = is_array($arr[$key . '.']);
577            if ($this->regexMode) {
578                // The value has matched
579                if (preg_match($searchPattern, $arr[$key])) {
580                    $this->tsbrowser_searchKeys[$depth] += 2;
581                }
582                // The key has matched
583                if (preg_match($searchPattern, $key)) {
584                    $this->tsbrowser_searchKeys[$depth] += 4;
585                }
586                // Just open this subtree if the parent key has matched the search
587                if (preg_match($searchPattern, $depth_in)) {
588                    $this->tsbrowser_searchKeys[$depth] = 1;
589                }
590            } else {
591                // The value has matched
592                if (stristr($arr[$key], $searchString)) {
593                    $this->tsbrowser_searchKeys[$depth] += 2;
594                }
595                // The key has matches
596                if (stristr($key, $searchString)) {
597                    $this->tsbrowser_searchKeys[$depth] += 4;
598                }
599                // Just open this subtree if the parent key has matched the search
600                if (stristr($depth_in, $searchString)) {
601                    $this->tsbrowser_searchKeys[$depth] = 1;
602                }
603            }
604            if ($deeper) {
605                $cS = count($this->tsbrowser_searchKeys);
606                $keyArray = $this->ext_getSearchKeys($arr[$key . '.'], $depth, $searchString, $keyArray);
607                if ($cS != count($this->tsbrowser_searchKeys)) {
608                    $keyArray[$depth] = 1;
609                }
610            }
611        }
612        return $keyArray;
613    }
614
615    /**
616     * @param int $pid
617     * @return int
618     */
619    public function ext_getRootlineNumber($pid)
620    {
621        if ($pid) {
622            foreach ($this->getRootLine() as $key => $val) {
623                if ((int)$val['uid'] === (int)$pid) {
624                    return (int)$key;
625                }
626            }
627        }
628        return -1;
629    }
630
631    /**
632     * @param array $arr
633     * @param string $depthData
634     * @param array $keyArray
635     * @param int $first
636     * @return array
637     */
638    public function ext_getTemplateHierarchyArr($arr, $depthData, $keyArray, $first = 0)
639    {
640        $keyArr = [];
641        foreach ($arr as $key => $value) {
642            $key = preg_replace('/\\.$/', '', $key);
643            if (substr($key, -1) !== '.') {
644                $keyArr[$key] = 1;
645            }
646        }
647        $a = 0;
648        $c = count($keyArr);
649        /** @var \TYPO3\CMS\Backend\Routing\UriBuilder $uriBuilder */
650        $uriBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Routing\UriBuilder::class);
651        /** @var IconFactory $iconFactory */
652        $iconFactory = GeneralUtility::makeInstance(IconFactory::class);
653        foreach ($keyArr as $key => $value) {
654            $HTML = '';
655            $a++;
656            $deeper = is_array($arr[$key . '.']);
657            $row = $arr[$key];
658            $LN = $a == $c ? 'blank' : 'line';
659            $BTM = $a == $c ? 'top' : '';
660            $HTML .= $depthData;
661            $alttext = '[' . $row['templateID'] . ']';
662            $alttext .= $row['pid'] ? ' - ' . BackendUtility::getRecordPath($row['pid'], '1=1', 20) : '';
663            $icon = strpos($row['templateID'], 'sys') === 0
664                ? '<span title="' . htmlspecialchars($alttext) . '">' . $iconFactory->getIconForRecord('sys_template', $row, Icon::SIZE_SMALL)->render() . '</span>'
665                : '<span title="' . htmlspecialchars($alttext) . '">' . $iconFactory->getIcon('mimetypes-x-content-template-static', Icon::SIZE_SMALL)->render() . '</span>';
666            if (in_array($row['templateID'], $this->clearList_const) || in_array($row['templateID'], $this->clearList_setup)) {
667                $urlParameters = [
668                    'id' => (int)GeneralUtility::_GP('id'),
669                    'template' => $row['templateID']
670                ];
671                $aHref = (string)$uriBuilder->buildUriFromRoute('web_ts', $urlParameters);
672                $A_B = '<a href="' . htmlspecialchars($aHref) . '">';
673                $A_E = '</a>';
674                if (GeneralUtility::_GP('template') == $row['templateID']) {
675                    $A_B = '<strong>' . $A_B;
676                    $A_E .= '</strong>';
677                }
678            } else {
679                $A_B = '';
680                $A_E = '';
681            }
682            $HTML .= ($first ? '' : '<span class="treeline-icon treeline-icon-join' . $BTM . '"></span>') . $icon . ' ' . $A_B
683                . htmlspecialchars(GeneralUtility::fixed_lgd_cs($row['title'], $GLOBALS['BE_USER']->uc['titleLen']))
684                . $A_E . '&nbsp;&nbsp;';
685            $RL = $this->ext_getRootlineNumber($row['pid']);
686            $statusCheckedIcon = $iconFactory->getIcon('status-status-checked', Icon::SIZE_SMALL)->render();
687            $keyArray[] = '<tr>
688							<td class="nowrap">' . $HTML . '</td>
689							<td align="center">' . ($row['root'] ? $statusCheckedIcon : '') . '</td>
690							<td align="center">' . ($row['clConf'] ? $statusCheckedIcon : '') . '</td>
691							<td align="center">' . ($row['clConst'] ? $statusCheckedIcon : '') . '</td>
692							<td align="center">' . ($row['pid'] ?: '') . '</td>
693							<td align="center">' . ($RL >= 0 ? $RL : '') . '</td>
694							<td>' . ($row['next'] ? $row['next'] : '') . '</td>
695						</tr>';
696            if ($deeper) {
697                $keyArray = $this->ext_getTemplateHierarchyArr($arr[$key . '.'], $depthData . ($first ? '' : '<span class="treeline-icon treeline-icon-' . $LN . '"></span>'), $keyArray);
698            }
699        }
700        return $keyArray;
701    }
702
703    /**
704     * Processes the flat array from TemplateService->hierarchyInfo
705     * and turns it into a hierarchical array to show dependencies (used by TemplateAnalyzer)
706     *
707     * @param array $depthDataArr (empty array on external call)
708     * @param int &$pointer Element number (1! to count()) of $this->hierarchyInfo that should be processed.
709     * @return array Processed hierachyInfo.
710     */
711    public function ext_process_hierarchyInfo(array $depthDataArr, &$pointer)
712    {
713        $parent = $this->hierarchyInfo[$pointer - 1]['templateParent'];
714        while ($pointer > 0 && $this->hierarchyInfo[$pointer - 1]['templateParent'] == $parent) {
715            $pointer--;
716            $row = $this->hierarchyInfo[$pointer];
717            $depthDataArr[$row['templateID']] = $row;
718            unset($this->clearList_setup_temp[$row['templateID']]);
719            unset($this->clearList_const_temp[$row['templateID']]);
720            $this->templateTitles[$row['templateID']] = $row['title'];
721            if ($row['templateID'] == $this->hierarchyInfo[$pointer - 1]['templateParent']) {
722                $depthDataArr[$row['templateID'] . '.'] = $this->ext_process_hierarchyInfo([], $pointer);
723            }
724        }
725        return $depthDataArr;
726    }
727
728    /**
729     * Get formatted HTML output for TypoScript either with Syntaxhiglighting or in plain mode
730     *
731     * @param array $config Array with simple strings of typoscript code.
732     * @param bool $lineNumbers Prepend linNumbers to each line.
733     * @param bool $comments Enable including comments in output.
734     * @param bool $crop Enable cropping of long lines.
735     * @param bool $syntaxHL Enrich output with syntaxhighlighting.
736     * @param int $syntaxHLBlockmode
737     * @return string
738     */
739    public function ext_outputTS(
740        array $config,
741        $lineNumbers = false,
742        $comments = false,
743        $crop = false,
744        $syntaxHL = false,
745        $syntaxHLBlockmode = 0
746    ) {
747        $all = '';
748        foreach ($config as $str) {
749            $all .= '[GLOBAL]' . LF . $str;
750        }
751        if ($syntaxHL) {
752            $tsparser = GeneralUtility::makeInstance(Parser\TypoScriptParser::class);
753            $tsparser->lineNumberOffset = $this->ext_lineNumberOffset + 1;
754            $tsparser->parentObject = $this;
755            return $tsparser->doSyntaxHighlight($all, $lineNumbers ? [$this->ext_lineNumberOffset + 1] : '', $syntaxHLBlockmode);
756        }
757        return $this->ext_formatTS($all, $lineNumbers, $comments, $crop);
758    }
759
760    /**
761     * Returns a new string of max. $chars length
762     * If the string is longer, it will be truncated and prepended with '...'
763     * $chars must be an integer of at least 4
764     *
765     * @param string $string
766     * @param int $chars
767     * @return string
768     */
769    public function ext_fixed_lgd($string, $chars)
770    {
771        if ($chars >= 4) {
772            if (strlen($string) > $chars) {
773                if (strlen($string) > 24 && preg_match('/^##[a-z0-9]{6}_B##$/', substr($string, 0, 12))) {
774                    $string = GeneralUtility::fixed_lgd_cs(substr($string, 12, -12), $chars - 3);
775                    $marker = substr(md5($string), 0, 6);
776                    return '##' . $marker . '_B##' . $string . '##' . $marker . '_E##';
777                }
778                return GeneralUtility::fixed_lgd_cs($string, $chars - 3);
779            }
780        }
781        return $string;
782    }
783
784    /**
785     * @param int $lineNumber Line Number
786     * @param array $str
787     * @return string
788     */
789    public function ext_lnBreakPointWrap($lineNumber, $str)
790    {
791        return '<a href="#" id="line-' . $lineNumber . '" onClick="return brPoint(' . $lineNumber . ','
792            . ($this->ext_lineNumberOffset_mode === 'setup' ? 1 : 0) . ');">' . $str . '</a>';
793    }
794
795    /**
796     * @param string $input
797     * @param bool $ln
798     * @param bool $comments
799     * @param bool $crop
800     * @return string
801     */
802    public function ext_formatTS($input, $ln, $comments = true, $crop = false)
803    {
804        $cArr = explode(LF, $input);
805        $n = ceil(log10(count($cArr) + $this->ext_lineNumberOffset));
806        $lineNum = '';
807        foreach ($cArr as $k => $v) {
808            $lln = $k + $this->ext_lineNumberOffset + 1;
809            if ($ln) {
810                $lineNum = $this->ext_lnBreakPointWrap($lln, str_replace(' ', '&nbsp;', sprintf('% ' . $n . 'd', $lln))) . ':   ';
811            }
812            $v = htmlspecialchars($v);
813            if ($crop) {
814                $v = $this->ext_fixed_lgd($v, $ln ? 71 : 77);
815            }
816            $cArr[$k] = $lineNum . str_replace(' ', '&nbsp;', $v);
817            $firstChar = substr(trim($v), 0, 1);
818            if ($firstChar === '[') {
819                $cArr[$k] = '<strong class="text-success">' . $cArr[$k] . '</strong>';
820            } elseif ($firstChar === '/' || $firstChar === '#') {
821                if ($comments) {
822                    $cArr[$k] = '<span class="text-muted">' . $cArr[$k] . '</span>';
823                } else {
824                    unset($cArr[$k]);
825                }
826            }
827        }
828        $output = implode('<br />', $cArr) . '<br />';
829        return $output;
830    }
831
832    /**
833     * Get a single sys_template record attached to a single page.
834     * If multiple template records are on this page, the first (order by sorting)
835     * record will be returned, unless a specific template uid is specified via $templateUid
836     *
837     * @param int $pid The pid to select sys_template records from
838     * @param int $templateUid Optional template uid
839     * @return array|null Returns the template record or null if none was found
840     */
841    public function ext_getFirstTemplate($pid, $templateUid = 0)
842    {
843        if (empty($pid)) {
844            return null;
845        }
846
847        // Query is taken from the runThroughTemplates($theRootLine) function in the parent class.
848        $queryBuilder = $this->getTemplateQueryBuilder($pid)
849            ->setMaxResults(1);
850        if ($templateUid) {
851            $queryBuilder->andWhere(
852                $queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($templateUid, \PDO::PARAM_INT))
853            );
854        }
855        $row = $queryBuilder->execute()->fetch();
856        BackendUtility::workspaceOL('sys_template', $row);
857
858        return $row;
859    }
860
861    /**
862     * Get an array of all template records on a page.
863     *
864     * @param int $pid Pid to fetch sys_template records for
865     * @return array[] Array of template records
866     */
867    public function ext_getAllTemplates($pid): array
868    {
869        if (empty($pid)) {
870            return [];
871        }
872        $result = $this->getTemplateQueryBuilder($pid)->execute();
873        $outRes = [];
874        while ($row = $result->fetch()) {
875            BackendUtility::workspaceOL('sys_template', $row);
876            if (is_array($row)) {
877                $outRes[] = $row;
878            }
879        }
880        return $outRes;
881    }
882
883    /**
884     * Internal helper method to prepare the query builder for
885     * getting sys_template records from a given pid
886     *
887     * @param int $pid The pid to select sys_template records from
888     * @return QueryBuilder Returns a QueryBuilder
889     */
890    protected function getTemplateQueryBuilder(int $pid): QueryBuilder
891    {
892        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
893            ->getQueryBuilderForTable('sys_template');
894        $queryBuilder->getRestrictions()
895            ->removeAll()
896            ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
897            ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class));
898
899        $queryBuilder->select('*')
900            ->from('sys_template')
901            ->where(
902                $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pid, \PDO::PARAM_INT))
903            );
904        if (!empty($GLOBALS['TCA']['sys_template']['ctrl']['sortby'])) {
905            $queryBuilder->orderBy($GLOBALS['TCA']['sys_template']['ctrl']['sortby']);
906        }
907
908        return $queryBuilder;
909    }
910
911    /**
912     * This function compares the flattened constants (default and all).
913     * Returns an array with the constants from the whole template which may be edited by the module.
914     *
915     * @param array $default
916     * @return array
917     */
918    public function ext_compareFlatSetups($default)
919    {
920        $editableComments = [];
921        $counter = 0;
922        foreach ($this->flatSetup as $const => $value) {
923            if (substr($const, -2) === '..' || !isset($this->flatSetup[$const . '..'])) {
924                continue;
925            }
926            $counter++;
927            $comment = trim($this->flatSetup[$const . '..']);
928            $c_arr = explode(LF, $comment);
929            foreach ($c_arr as $k => $v) {
930                $line = trim(preg_replace('/^[#\\/]*/', '', $v));
931                if (!$line) {
932                    continue;
933                }
934                $parts = explode(';', $line);
935                foreach ($parts as $par) {
936                    if (strstr($par, '=')) {
937                        $keyValPair = explode('=', $par, 2);
938                        switch (trim(strtolower($keyValPair[0]))) {
939                            case 'type':
940                                // Type:
941                                $editableComments[$const]['type'] = trim($keyValPair[1]);
942                                break;
943                            case 'cat':
944                                // List of categories.
945                                $catSplit = explode('/', strtolower($keyValPair[1]));
946                                $catSplit[0] = trim($catSplit[0]);
947                                if (isset($this->categoryLabels[$catSplit[0]])) {
948                                    $catSplit[0] = $this->categoryLabels[$catSplit[0]];
949                                }
950                                $editableComments[$const]['cat'] = $catSplit[0];
951                                // This is the subcategory. Must be a key in $this->subCategories[].
952                                // catSplit[2] represents the search-order within the subcat.
953                                $catSplit[1] = trim($catSplit[1]);
954                                if ($catSplit[1] && isset($this->subCategories[$catSplit[1]])) {
955                                    $editableComments[$const]['subcat_name'] = $catSplit[1];
956                                    $orderIdentifier = isset($catSplit[2]) ? trim($catSplit[2]) : $counter;
957                                    $editableComments[$const]['subcat'] = $this->subCategories[$catSplit[1]][1]
958                                        . '/' . $catSplit[1] . '/' . $orderIdentifier . 'z';
959                                } elseif (isset($catSplit[2])) {
960                                    $editableComments[$const]['subcat'] = 'x' . '/' . trim($catSplit[2]) . 'z';
961                                } else {
962                                    $editableComments[$const]['subcat'] = 'x' . '/' . $counter . 'z';
963                                }
964                                break;
965                            case 'label':
966                                // Label
967                                $editableComments[$const]['label'] = trim($keyValPair[1]);
968                                break;
969                            case 'customcategory':
970                                // Custom category label
971                                $customCategory = explode('=', $keyValPair[1], 2);
972                                if (trim($customCategory[0])) {
973                                    $categoryKey = strtolower($customCategory[0]);
974                                    $this->categoryLabels[$categoryKey] = $this->getLanguageService()->sL($customCategory[1]);
975                                }
976                                break;
977                            case 'customsubcategory':
978                                // Custom subCategory label
979                                $customSubcategory = explode('=', $keyValPair[1], 2);
980                                if (trim($customSubcategory[0])) {
981                                    $subCategoryKey = strtolower($customSubcategory[0]);
982                                    $this->subCategories[$subCategoryKey][0] = $this->getLanguageService()->sL($customSubcategory[1]);
983                                }
984                                break;
985                        }
986                    }
987                }
988            }
989            if (isset($editableComments[$const])) {
990                $editableComments[$const]['name'] = $const;
991                $editableComments[$const]['value'] = trim($value);
992                if (isset($default[$const])) {
993                    $editableComments[$const]['default_value'] = trim($default[$const]);
994                }
995            }
996        }
997        return $editableComments;
998    }
999
1000    /**
1001     * @param array $editConstArray
1002     */
1003    public function ext_categorizeEditableConstants($editConstArray)
1004    {
1005        // Runs through the available constants and fills the $this->categories array with pointers and priority-info
1006        foreach ($editConstArray as $constName => $constData) {
1007            if (!$constData['type']) {
1008                $constData['type'] = 'string';
1009            }
1010            $cats = explode(',', $constData['cat']);
1011            // if = only one category, while allows for many. We have agreed on only one category is the most basic way...
1012            foreach ($cats as $theCat) {
1013                $theCat = trim($theCat);
1014                if ($theCat) {
1015                    $this->categories[$theCat][$constName] = $constData['subcat'];
1016                }
1017            }
1018        }
1019    }
1020
1021    /**
1022     * @return array
1023     */
1024    public function ext_getCategoryLabelArray()
1025    {
1026        // Returns array used for labels in the menu.
1027        $retArr = [];
1028        foreach ($this->categories as $k => $v) {
1029            if (!empty($v)) {
1030                $retArr[$k] = strtoupper($k) . ' (' . count($v) . ')';
1031            }
1032        }
1033        return $retArr;
1034    }
1035
1036    /**
1037     * @param string $type
1038     * @return array
1039     */
1040    public function ext_getTypeData($type)
1041    {
1042        $retArr = [];
1043        $type = trim($type);
1044        if (!$type) {
1045            $retArr['type'] = 'string';
1046        } else {
1047            $m = strcspn($type, ' [');
1048            $retArr['type'] = strtolower(substr($type, 0, $m));
1049            $types = ['int' => 1, 'options' => 1, 'file' => 1, 'boolean' => 1, 'offset' => 1, 'user' => 1];
1050            if (isset($types[$retArr['type']])) {
1051                $p = trim(substr($type, $m));
1052                $reg = [];
1053                preg_match('/\\[(.*)\\]/', $p, $reg);
1054                $p = trim($reg[1]);
1055                if ($p) {
1056                    $retArr['paramstr'] = $p;
1057                    switch ($retArr['type']) {
1058                        case 'int':
1059                            if ($retArr['paramstr'][0] === '-') {
1060                                $retArr['params'] = GeneralUtility::intExplode('-', substr($retArr['paramstr'], 1));
1061                                $retArr['params'][0] = (int)('-' . $retArr['params'][0]);
1062                            } else {
1063                                $retArr['params'] = GeneralUtility::intExplode('-', $retArr['paramstr']);
1064                            }
1065                            $retArr['min'] = $retArr['params'][0];
1066                            $retArr['max'] = $retArr['params'][1];
1067                            $retArr['paramstr'] = $retArr['params'][0] . ' - ' . $retArr['params'][1];
1068                            break;
1069                        case 'options':
1070                            $retArr['params'] = explode(',', $retArr['paramstr']);
1071                            break;
1072                    }
1073                }
1074            }
1075        }
1076        return $retArr;
1077    }
1078
1079    /**
1080     * @param array $params
1081     * @return array
1082     */
1083    public function ext_fNandV($params)
1084    {
1085        $fN = 'data[' . $params['name'] . ']';
1086        $idName = str_replace('.', '-', $params['name']);
1087        $fV = $params['value'];
1088        // Values entered from the constantsedit cannot be constants!	230502; removed \{ and set {
1089        if (preg_match('/^{[\\$][a-zA-Z0-9\\.]*}$/', trim($fV), $reg)) {
1090            $fV = '';
1091        }
1092        $fV = htmlspecialchars($fV);
1093        return [$fN, $fV, $params, $idName];
1094    }
1095
1096    /**
1097     * This functions returns the HTML-code that creates the editor-layout of the module.
1098     *
1099     * @param array $theConstants
1100     * @param string $category
1101     * @return string
1102     */
1103    public function ext_printFields($theConstants, $category)
1104    {
1105        reset($theConstants);
1106        $output = '';
1107        $subcat = '';
1108        if (is_array($this->categories[$category])) {
1109            if (!$this->doNotSortCategoriesBeforeMakingForm) {
1110                asort($this->categories[$category]);
1111            }
1112            /** @var IconFactory $iconFactory */
1113            $iconFactory = GeneralUtility::makeInstance(IconFactory::class);
1114            foreach ($this->categories[$category] as $name => $type) {
1115                $params = $theConstants[$name];
1116                if (is_array($params)) {
1117                    if ($subcat != $params['subcat_name']) {
1118                        $subcat = $params['subcat_name'];
1119                        $subcat_name = $params['subcat_name'] ? $this->subCategories[$params['subcat_name']][0] : 'Others';
1120                        $output .= '<h3>' . $subcat_name . '</h3>';
1121                    }
1122                    $label = $this->getLanguageService()->sL($params['label']);
1123                    $label_parts = explode(':', $label, 2);
1124                    if (count($label_parts) === 2) {
1125                        $head = trim($label_parts[0]);
1126                        $body = trim($label_parts[1]);
1127                    } else {
1128                        $head = trim($label_parts[0]);
1129                        $body = '';
1130                    }
1131                    $typeDat = $this->ext_getTypeData($params['type']);
1132                    $p_field = '';
1133                    $raname = substr(md5($params['name']), 0, 10);
1134                    $aname = '\'' . $raname . '\'';
1135                    list($fN, $fV, $params, $idName) = $this->ext_fNandV($params);
1136                    $idName = htmlspecialchars($idName);
1137                    $hint = '';
1138                    switch ($typeDat['type']) {
1139                        case 'int':
1140                        case 'int+':
1141                            $additionalAttributes = '';
1142                            if ($typeDat['paramstr']) {
1143                                $hint = ' Range: ' . $typeDat['paramstr'];
1144                            } elseif ($typeDat['type'] === 'int+') {
1145                                $hint = ' Range: 0 - ';
1146                                $typeDat['min'] = 0;
1147                            } else {
1148                                $hint = ' (Integer)';
1149                            }
1150
1151                            if (isset($typeDat['min'])) {
1152                                $additionalAttributes .= ' min="' . (int)$typeDat['min'] . '" ';
1153                            }
1154                            if (isset($typeDat['max'])) {
1155                                $additionalAttributes .= ' max="' . (int)$typeDat['max'] . '" ';
1156                            }
1157
1158                            $p_field =
1159                                '<input class="form-control" id="' . $idName . '" type="number"'
1160                                . ' name="' . $fN . '" value="' . $fV . '"' . ' onChange="uFormUrl(' . $aname . ')"' . $additionalAttributes . ' />';
1161                            break;
1162                        case 'color':
1163                            $p_field = '
1164                                <input class="form-control formengine-colorpickerelement t3js-color-picker" type="text" id="input-' . $idName . '" rel="' . $idName .
1165                                '" name="' . $fN . '" value="' . $fV . '" onChange="uFormUrl(' . $aname . ')" />';
1166
1167                            if (empty($this->inlineJavaScript[$typeDat['type']])) {
1168                                $this->inlineJavaScript[$typeDat['type']] = 'require([\'TYPO3/CMS/Backend/ColorPicker\'], function(ColorPicker){ColorPicker.initialize()});';
1169                            }
1170                            break;
1171                        case 'wrap':
1172                            $wArr = explode('|', $fV);
1173                            $p_field = '<div class="input-group">
1174                                            <input class="form-control form-control-adapt" type="text" id="' . $idName . '" name="' . $fN . '" value="' . $wArr[0] . '" onChange="uFormUrl(' . $aname . ')" />
1175                                            <span class="input-group-addon input-group-icon">|</span>
1176                                            <input class="form-control form-control-adapt" type="text" name="W' . $fN . '" value="' . $wArr[1] . '" onChange="uFormUrl(' . $aname . ')" />
1177                                         </div>';
1178                            break;
1179                        case 'offset':
1180                            $wArr = explode(',', $fV);
1181                            $labels = GeneralUtility::trimExplode(',', $typeDat['paramstr']);
1182                            $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] . '" onChange="uFormUrl(' . $aname . ')" />';
1183                            $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" onChange="uFormUrl(' . $aname . ')" />';
1184                            $labelsCount = count($labels);
1185                            for ($aa = 2; $aa < $labelsCount; $aa++) {
1186                                if ($labels[$aa]) {
1187                                    $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" onChange="uFormUrl(' . $aname . ')" />';
1188                                } else {
1189                                    $p_field .= '<input type="hidden" name="W' . $aa . $fN . '" value="' . $wArr[$aa] . '" />';
1190                                }
1191                            }
1192                            $p_field = '<div class="input-group">' . $p_field . '</div>';
1193                            break;
1194                        case 'options':
1195                            if (is_array($typeDat['params'])) {
1196                                $p_field = '';
1197                                foreach ($typeDat['params'] as $val) {
1198                                    $vParts = explode('=', $val, 2);
1199                                    $label = $vParts[0];
1200                                    $val = $vParts[1] ?? $vParts[0];
1201                                    // option tag:
1202                                    $sel = '';
1203                                    if ($val === $params['value']) {
1204                                        $sel = ' selected';
1205                                    }
1206                                    $p_field .= '<option value="' . htmlspecialchars($val) . '"' . $sel . '>' . $this->getLanguageService()->sL($label) . '</option>';
1207                                }
1208                                $p_field = '<select class="form-control" id="' . $idName . '" name="' . $fN . '" onChange="uFormUrl(' . $aname . ')">' . $p_field . '</select>';
1209                            }
1210                            break;
1211                        case 'boolean':
1212                            $sel = $fV ? 'checked' : '';
1213                            $p_field =
1214                                '<input type="hidden" name="' . $fN . '" value="0" />'
1215                                . '<label class="btn btn-default btn-checkbox">'
1216                                . '<input id="' . $idName . '" type="checkbox" name="' . $fN . '" value="' . ($typeDat['paramstr'] ? $typeDat['paramstr'] : 1) . '" ' . $sel . ' onClick="uFormUrl(' . $aname . ')" />'
1217                                . '<span class="t3-icon fa"></span>'
1218                                . '</label>';
1219                            break;
1220                        case 'comment':
1221                            $sel = $fV ? '' : 'checked';
1222                            $p_field =
1223                                '<input type="hidden" name="' . $fN . '" value="" />'
1224                                . '<label class="btn btn-default btn-checkbox">'
1225                                . '<input id="' . $idName . '" type="checkbox" name="' . $fN . '" value="1" ' . $sel . ' onClick="uFormUrl(' . $aname . ')" />'
1226                                . '<span class="t3-icon fa"></span>'
1227                                . '</label>';
1228                            break;
1229                        case 'file':
1230                            // extensionlist
1231                            $extList = $typeDat['paramstr'];
1232                            if ($extList === 'IMAGE_EXT') {
1233                                $extList = $GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext'];
1234                            }
1235                            $p_field = '<option value="">(' . $extList . ')</option>';
1236                            if (trim($params['value'])) {
1237                                $val = $params['value'];
1238                                $p_field .= '<option value=""></option>';
1239                                $p_field .= '<option value="' . htmlspecialchars($val) . '" selected>' . $val . '</option>';
1240                            }
1241                            $p_field = '<select class="form-select" id="' . $idName . '" name="' . $fN . '" onChange="uFormUrl(' . $aname . ')">' . $p_field . '</select>';
1242                            break;
1243                        case 'user':
1244                            $userFunction = $typeDat['paramstr'];
1245                            $userFunctionParams = ['fieldName' => $fN, 'fieldValue' => $fV];
1246                            $p_field = GeneralUtility::callUserFunction($userFunction, $userFunctionParams, $this);
1247                            break;
1248                        default:
1249                            $p_field = '<input class="form-control" id="' . $idName . '" type="text" name="' . $fN . '" value="' . $fV . '"'
1250                                . ' onChange="uFormUrl(' . $aname . ')" />';
1251                    }
1252                    // Define default names and IDs
1253                    $userTyposcriptID = 'userTS-' . $idName;
1254                    $defaultTyposcriptID = 'defaultTS-' . $idName;
1255                    $checkboxName = 'check[' . $params['name'] . ']';
1256                    $checkboxID = 'check-' . $idName;
1257                    $userTyposcriptStyle = '';
1258                    $deleteIconHTML = '';
1259                    $constantCheckbox = '';
1260                    $constantDefaultRow = '';
1261                    if (!$this->ext_dontCheckIssetValues) {
1262                        // Set the default styling options
1263                        if (isset($this->objReg[$params['name']])) {
1264                            $checkboxValue = 'checked';
1265                            $defaultTyposcriptStyle = 'style="display:none;"';
1266                        } else {
1267                            $checkboxValue = '';
1268                            $userTyposcriptStyle = 'style="display:none;"';
1269                            $defaultTyposcriptStyle = '';
1270                        }
1271                        $deleteTitle = htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.deleteTitle'));
1272                        $deleteIcon = $iconFactory->getIcon('actions-edit-undo', Icon::SIZE_SMALL)->render();
1273                        $deleteIconHTML =
1274                            '<button type="button" class="btn btn-default t3js-toggle" data-toggle="undo" rel="' . $idName . '">'
1275                                . '<span title="' . $deleteTitle . '" alt="' . $deleteTitle . '">'
1276                                    . $deleteIcon
1277                                . '</span>'
1278                            . '</button>';
1279                        $editTitle = htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.editTitle'));
1280                        $editIcon = $iconFactory->getIcon('actions-open', Icon::SIZE_SMALL)->render();
1281                        $editIconHTML =
1282                            '<button type="button" class="btn btn-default t3js-toggle" data-toggle="edit"  rel="' . $idName . '">'
1283                                . '<span title="' . $editTitle . '" alt="' . $editTitle . '">'
1284                                    . $editIcon
1285                                . '</span>'
1286                            . '</button>';
1287                        $constantCheckbox = '<input type="hidden" name="' . $checkboxName . '" id="' . $checkboxID . '" value="' . $checkboxValue . '"/>';
1288                        // If there's no default value for the field, use a static label.
1289                        if (!$params['default_value']) {
1290                            $params['default_value'] = '[Empty]';
1291                        }
1292                        $constantDefaultRow =
1293                            '<div class="input-group defaultTS" id="' . $defaultTyposcriptID . '" ' . $defaultTyposcriptStyle . '>'
1294                                . '<span class="input-group-btn">' . $editIconHTML . '</span>'
1295                                . '<input class="form-control" type="text" placeholder="' . htmlspecialchars($params['default_value']) . '" readonly>'
1296                            . '</div>';
1297                    }
1298                    $constantEditRow =
1299                        '<div class="input-group userTS" id="' . $userTyposcriptID . '" ' . $userTyposcriptStyle . '>'
1300                            . '<span class="input-group-btn">' . $deleteIconHTML . '</span>'
1301                            . $p_field
1302                        . '</div>';
1303                    $constantLabel = '<label class="t3js-formengine-label"><span>' . htmlspecialchars($head) . '</span></label>';
1304                    $constantName = '<span class="help-block">[' . $params['name'] . ']</span>';
1305                    $constantDescription = $body ? '<p class="help-block">' . htmlspecialchars($body) . '</p>' : '';
1306                    $constantData = '';
1307                    if ($hint !== '') {
1308                        $constantData .= '<span class="help-block">' . $hint . '</span>';
1309                    }
1310                    $constantData .=
1311                        $constantCheckbox
1312                        . $constantEditRow
1313                        . $constantDefaultRow;
1314
1315                    $output .=
1316                        '<fieldset class="form-section">'
1317                            . '<a name="' . $raname . '"></a>'
1318                            . '<div class="form-group">'
1319                                . $constantLabel . $constantName . $constantDescription . $constantData
1320                            . '</div>'
1321                        . '</fieldset>';
1322                } else {
1323                    debug('Error. Constant did not exist. Should not happen.');
1324                }
1325            }
1326        }
1327        return '<div class="tstemplate-constanteditor">' . $output . '</div>';
1328    }
1329
1330    /***************************
1331     *
1332     * Processing input values
1333     *
1334     ***************************/
1335    /**
1336     * @param string $constants
1337     */
1338    public function ext_regObjectPositions($constants)
1339    {
1340        // This runs through the lines of the constants-field of the active template and registers the constants-names
1341        // and line positions in an array, $this->objReg
1342        $this->raw = explode(LF, $constants);
1343        $this->rawP = 0;
1344        // Resetting the objReg if the divider is found!!
1345        $this->objReg = [];
1346        $this->ext_regObjects('');
1347    }
1348
1349    /**
1350     * @param string $pre
1351     */
1352    public function ext_regObjects($pre)
1353    {
1354        // Works with regObjectPositions. "expands" the names of the TypoScript objects
1355        while (isset($this->raw[$this->rawP])) {
1356            $line = ltrim($this->raw[$this->rawP]);
1357            $this->rawP++;
1358            if ($line) {
1359                if ($line[0] === '[') {
1360                } elseif (strcspn($line, '}#/') != 0) {
1361                    $varL = strcspn($line, ' {=<');
1362                    $var = substr($line, 0, $varL);
1363                    $line = ltrim(substr($line, $varL));
1364                    switch ($line[0]) {
1365                        case '=':
1366                            $this->objReg[$pre . $var] = $this->rawP - 1;
1367                            break;
1368                        case '{':
1369                            $this->ext_inBrace++;
1370                            $this->ext_regObjects($pre . $var . '.');
1371                            break;
1372                    }
1373                    $this->lastComment = '';
1374                } elseif ($line[0] === '}') {
1375                    $this->lastComment = '';
1376                    $this->ext_inBrace--;
1377                    if ($this->ext_inBrace < 0) {
1378                        $this->ext_inBrace = 0;
1379                    } else {
1380                        break;
1381                    }
1382                }
1383            }
1384        }
1385    }
1386
1387    /**
1388     * @param string $key
1389     * @param string $var
1390     */
1391    public function ext_putValueInConf($key, $var)
1392    {
1393        // Puts the value $var to the TypoScript value $key in the current lines of the templates.
1394        // If the $key is not found in the template constants field, a new line is inserted in the bottom.
1395        $theValue = ' ' . trim($var);
1396        if (isset($this->objReg[$key])) {
1397            $lineNum = $this->objReg[$key];
1398            $parts = explode('=', $this->raw[$lineNum], 2);
1399            if (count($parts) === 2) {
1400                $parts[1] = $theValue;
1401            }
1402            $this->raw[$lineNum] = implode('=', $parts);
1403        } else {
1404            $this->raw[] = $key . ' =' . $theValue;
1405        }
1406        $this->changed = true;
1407    }
1408
1409    /**
1410     * @param string $key
1411     */
1412    public function ext_removeValueInConf($key)
1413    {
1414        // Removes the value in the configuration
1415        if (isset($this->objReg[$key])) {
1416            $lineNum = $this->objReg[$key];
1417            unset($this->raw[$lineNum]);
1418        }
1419        $this->changed = true;
1420    }
1421
1422    /**
1423     * @param array $arr
1424     * @param array $settings
1425     * @return array
1426     */
1427    public function ext_depthKeys($arr, $settings)
1428    {
1429        $tsbrArray = [];
1430        foreach ($arr as $theK => $theV) {
1431            $theKeyParts = explode('.', $theK);
1432            $depth = '';
1433            $c = count($theKeyParts);
1434            $a = 0;
1435            foreach ($theKeyParts as $p) {
1436                $a++;
1437                $depth .= ($depth ? '.' : '') . $p;
1438                $tsbrArray[$depth] = $c == $a ? $theV : 1;
1439            }
1440        }
1441        // Modify settings
1442        foreach ($tsbrArray as $theK => $theV) {
1443            if ($theV) {
1444                $settings[$theK] = 1;
1445            } else {
1446                unset($settings[$theK]);
1447            }
1448        }
1449        return $settings;
1450    }
1451
1452    /**
1453     * Process input
1454     *
1455     * @param array $http_post_vars
1456     * @param array $http_post_files (not used anymore)
1457     * @param array $theConstants
1458     * @param array $tplRow Not used
1459     */
1460    public function ext_procesInput($http_post_vars, $http_post_files, $theConstants, $tplRow)
1461    {
1462        $data = $http_post_vars['data'];
1463        $check = $http_post_vars['check'];
1464        $Wdata = $http_post_vars['Wdata'];
1465        $W2data = $http_post_vars['W2data'];
1466        $W3data = $http_post_vars['W3data'];
1467        $W4data = $http_post_vars['W4data'];
1468        $W5data = $http_post_vars['W5data'];
1469        if (is_array($data)) {
1470            foreach ($data as $key => $var) {
1471                if (isset($theConstants[$key])) {
1472                    // If checkbox is set, update the value
1473                    if ($this->ext_dontCheckIssetValues || isset($check[$key])) {
1474                        // Exploding with linebreak, just to make sure that no multiline input is given!
1475                        list($var) = explode(LF, $var);
1476                        $typeDat = $this->ext_getTypeData($theConstants[$key]['type']);
1477                        switch ($typeDat['type']) {
1478                            case 'int':
1479                                if ($typeDat['paramstr']) {
1480                                    $var = MathUtility::forceIntegerInRange($var, $typeDat['params'][0], $typeDat['params'][1]);
1481                                } else {
1482                                    $var = (int)$var;
1483                                }
1484                                break;
1485                            case 'int+':
1486                                $var = max(0, (int)$var);
1487                                break;
1488                            case 'color':
1489                                $col = [];
1490                                if ($var) {
1491                                    $var = preg_replace('/[^A-Fa-f0-9]*/', '', $var);
1492                                    $useFulHex = strlen($var) > 3;
1493                                    $col[] = hexdec($var[0]);
1494                                    $col[] = hexdec($var[1]);
1495                                    $col[] = hexdec($var[2]);
1496                                    if ($useFulHex) {
1497                                        $col[] = hexdec($var[3]);
1498                                        $col[] = hexdec($var[4]);
1499                                        $col[] = hexdec($var[5]);
1500                                    }
1501                                    $var = substr('0' . dechex($col[0]), -1) . substr('0' . dechex($col[1]), -1) . substr('0' . dechex($col[2]), -1);
1502                                    if ($useFulHex) {
1503                                        $var .= substr('0' . dechex($col[3]), -1) . substr('0' . dechex($col[4]), -1) . substr('0' . dechex($col[5]), -1);
1504                                    }
1505                                    $var = '#' . strtoupper($var);
1506                                }
1507                                break;
1508                            case 'comment':
1509                                if ($var) {
1510                                    $var = '';
1511                                } else {
1512                                    $var = '#';
1513                                }
1514                                break;
1515                            case 'wrap':
1516                                if (isset($Wdata[$key])) {
1517                                    $var .= '|' . $Wdata[$key];
1518                                }
1519                                break;
1520                            case 'offset':
1521                                if (isset($Wdata[$key])) {
1522                                    $var = (int)$var . ',' . (int)$Wdata[$key];
1523                                    if (isset($W2data[$key])) {
1524                                        $var .= ',' . (int)$W2data[$key];
1525                                        if (isset($W3data[$key])) {
1526                                            $var .= ',' . (int)$W3data[$key];
1527                                            if (isset($W4data[$key])) {
1528                                                $var .= ',' . (int)$W4data[$key];
1529                                                if (isset($W5data[$key])) {
1530                                                    $var .= ',' . (int)$W5data[$key];
1531                                                }
1532                                            }
1533                                        }
1534                                    }
1535                                }
1536                                break;
1537                            case 'boolean':
1538                                if ($var) {
1539                                    $var = $typeDat['paramstr'] ? $typeDat['paramstr'] : 1;
1540                                }
1541                                break;
1542                        }
1543                        if ($this->ext_printAll || (string)$theConstants[$key]['value'] !== (string)$var) {
1544                            // Put value in, if changed.
1545                            $this->ext_putValueInConf($key, $var);
1546                        }
1547                        // Remove the entry because it has been "used"
1548                        unset($check[$key]);
1549                    } else {
1550                        $this->ext_removeValueInConf($key);
1551                    }
1552                }
1553            }
1554        }
1555        // Remaining keys in $check indicates fields that are just clicked "on" to be edited.
1556        // Therefore we get the default value and puts that in the template as a start...
1557        if (!$this->ext_dontCheckIssetValues && is_array($check)) {
1558            foreach ($check as $key => $var) {
1559                if (isset($theConstants[$key])) {
1560                    $dValue = $theConstants[$key]['default_value'];
1561                    $this->ext_putValueInConf($key, $dValue);
1562                }
1563            }
1564        }
1565    }
1566
1567    /**
1568     * @param int $id
1569     * @param string $perms_clause
1570     * @return array
1571     */
1572    public function ext_prevPageWithTemplate($id, $perms_clause)
1573    {
1574        $rootLine = BackendUtility::BEgetRootLine($id, $perms_clause ? ' AND ' . $perms_clause : '');
1575        foreach ($rootLine as $p) {
1576            if ($this->ext_getFirstTemplate($p['uid'])) {
1577                return $p;
1578            }
1579        }
1580        return [];
1581    }
1582
1583    /**
1584     * Is set by runThroughTemplates(), previously set via TemplateAnalyzerModuleFunctionController from the outside
1585     *
1586     * @return array
1587     */
1588    protected function getRootLine()
1589    {
1590        return is_array($this->absoluteRootLine) ? $this->absoluteRootLine : [];
1591    }
1592
1593    /**
1594     * @return LanguageService
1595     */
1596    protected function getLanguageService()
1597    {
1598        return $GLOBALS['LANG'];
1599    }
1600
1601    /**
1602     * @return DocumentTemplate
1603     */
1604    protected function getDocumentTemplate()
1605    {
1606        return $GLOBALS['TBE_TEMPLATE'];
1607    }
1608}
1609