1<?php
2namespace TYPO3\CMS\Core\TimeTracker;
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 */
16use TYPO3\CMS\Core\Imaging\Icon;
17use TYPO3\CMS\Core\Imaging\IconFactory;
18use TYPO3\CMS\Core\SingletonInterface;
19use TYPO3\CMS\Core\Utility\GeneralUtility;
20
21/**
22 * Frontend Timetracking functions
23 *
24 * Is used to register how much time is used with operations in TypoScript
25 */
26class TimeTracker implements SingletonInterface
27{
28    /**
29     * If set to true (see constructor) then then the timetracking is enabled
30     * @var bool
31     */
32    protected $isEnabled = false;
33
34    /**
35     * Is loaded with the millisecond time when this object is created
36     *
37     * @var int
38     */
39    public $starttime = 0;
40
41    /**
42     * Log Rendering flag. If set, ->push() and ->pull() is called from the cObj->cObjGetSingle().
43     * This determines whether or not the TypoScript parsing activity is logged. But it also slows down the rendering
44     *
45     * @var bool
46     */
47    public $LR = true;
48
49    /**
50     * @var array
51     */
52    public $printConf = [
53        'showParentKeys' => 1,
54        'contentLength' => 10000,
55        // Determines max length of displayed content before it gets cropped.
56        'contentLength_FILE' => 400,
57        // Determines max length of displayed content FROM FILE cObjects before it gets cropped. Reason is that most FILE cObjects are huge and often used as template-code.
58        'flag_tree' => 1,
59        'flag_messages' => 1,
60        'flag_content' => 0,
61        'allTime' => 0,
62        'keyLgd' => 40
63    ];
64
65    /**
66     * @var array
67     */
68    public $wrapError = [
69        0 => ['', ''],
70        1 => ['<strong>', '</strong>'],
71        2 => ['<strong style="color:#ff6600;">', '</strong>'],
72        3 => ['<strong style="color:#ff0000;">', '</strong>']
73    ];
74
75    /**
76     * @var array
77     */
78    public $wrapIcon = [
79        0 => '',
80        1 => 'actions-document-info',
81        2 => 'status-dialog-warning',
82        3 => 'status-dialog-error'
83    ];
84
85    /**
86     * @var int
87     */
88    public $uniqueCounter = 0;
89
90    /**
91     * @var array
92     */
93    public $tsStack = [[]];
94
95    /**
96     * @var int
97     */
98    public $tsStackLevel = 0;
99
100    /**
101     * @var array
102     */
103    public $tsStackLevelMax = [];
104
105    /**
106     * @var array
107     */
108    public $tsStackLog = [];
109
110    /**
111     * @var int
112     */
113    public $tsStackPointer = 0;
114
115    /**
116     * @var array
117     */
118    public $currentHashPointer = [];
119
120    /**
121     * Log entries that take than this number of milliseconds (own time) will be highlighted during log display. Set 0 to disable highlighting.
122     *
123     * @var int
124     */
125    public $highlightLongerThan = 0;
126
127    /*******************************************
128     *
129     * Logging parsing times in the scripts
130     *
131     *******************************************/
132
133    /**
134     * TimeTracker constructor.
135     *
136     * @param bool $isEnabled
137     */
138    public function __construct($isEnabled = true)
139    {
140        $this->isEnabled = $isEnabled;
141    }
142
143    /**
144     * Sets the starting time
145     */
146    public function start()
147    {
148        if (!$this->isEnabled) {
149            return;
150        }
151        $this->starttime = $this->getMilliseconds();
152    }
153
154    /**
155     * Pushes an element to the TypoScript tracking array
156     *
157     * @param string $tslabel Label string for the entry, eg. TypoScript property name
158     * @param string $value Additional value(?)
159     * @see \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer::cObjGetSingle(), pull()
160     */
161    public function push($tslabel, $value = '')
162    {
163        if (!$this->isEnabled) {
164            return;
165        }
166        $this->tsStack[$this->tsStackPointer][] = $tslabel;
167        $this->currentHashPointer[] = 'timetracker_' . $this->uniqueCounter++;
168        $this->tsStackLevel++;
169        $this->tsStackLevelMax[] = $this->tsStackLevel;
170        // setTSlog
171        $k = end($this->currentHashPointer);
172        $this->tsStackLog[$k] = [
173            'level' => $this->tsStackLevel,
174            'tsStack' => $this->tsStack,
175            'value' => $value,
176            'starttime' => microtime(true),
177            'stackPointer' => $this->tsStackPointer
178        ];
179    }
180
181    /**
182     * Pulls an element from the TypoScript tracking array
183     *
184     * @param string $content The content string generated within the push/pull part.
185     * @see \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer::cObjGetSingle(), push()
186     */
187    public function pull($content = '')
188    {
189        if (!$this->isEnabled) {
190            return;
191        }
192        $k = end($this->currentHashPointer);
193        $this->tsStackLog[$k]['endtime'] = microtime(true);
194        $this->tsStackLog[$k]['content'] = $content;
195        $this->tsStackLevel--;
196        array_pop($this->tsStack[$this->tsStackPointer]);
197        array_pop($this->currentHashPointer);
198    }
199
200    /**
201     * Logs the TypoScript entry
202     *
203     * @param string $content The message string
204     * @param int $num Message type: 0: information, 1: message, 2: warning, 3: error
205     * @see \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer::CONTENT()
206     */
207    public function setTSlogMessage($content, $num = 0)
208    {
209        if (!$this->isEnabled) {
210            return;
211        }
212        end($this->currentHashPointer);
213        $k = current($this->currentHashPointer);
214        $placeholder = '';
215        // Enlarge the "details" column by adding a span
216        if (strlen($content) > 30) {
217            $placeholder = '<br /><span style="width: 300px; height: 1px; display: inline-block;"></span>';
218        }
219        $iconFactory = GeneralUtility::makeInstance(IconFactory::class);
220        $this->tsStackLog[$k]['message'][] = $iconFactory->getIcon($this->wrapIcon[$num], Icon::SIZE_SMALL)->render() . $this->wrapError[$num][0] . htmlspecialchars($content) . $this->wrapError[$num][1] . $placeholder;
221    }
222
223    /**
224     * Set TSselectQuery - for messages in TypoScript debugger.
225     *
226     * @param array $data Query array
227     * @param string $msg Message/Label to attach
228     */
229    public function setTSselectQuery(array $data, $msg = '')
230    {
231        if (!$this->isEnabled) {
232            return;
233        }
234        end($this->currentHashPointer);
235        $k = current($this->currentHashPointer);
236        if ($msg !== '') {
237            $data['msg'] = $msg;
238        }
239        $this->tsStackLog[$k]['selectQuery'][] = $data;
240    }
241
242    /**
243     * Increases the stack pointer
244     *
245     * @see decStackPointer(), \TYPO3\CMS\Frontend\Page\PageGenerator::renderContent(), \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer::cObjGetSingle()
246     */
247    public function incStackPointer()
248    {
249        if (!$this->isEnabled) {
250            return;
251        }
252        $this->tsStackPointer++;
253        $this->tsStack[$this->tsStackPointer] = [];
254    }
255
256    /**
257     * Decreases the stack pointer
258     *
259     * @see incStackPointer(), \TYPO3\CMS\Frontend\Page\PageGenerator::renderContent(), \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer::cObjGetSingle()
260     */
261    public function decStackPointer()
262    {
263        if (!$this->isEnabled) {
264            return;
265        }
266        unset($this->tsStack[$this->tsStackPointer]);
267        $this->tsStackPointer--;
268    }
269
270    /**
271     * Gets a microtime value as milliseconds value.
272     *
273     * @param float $microtime The microtime value - if not set the current time is used
274     * @return int The microtime value as milliseconds value
275     */
276    public function getMilliseconds($microtime = null)
277    {
278        if (!$this->isEnabled) {
279            return 0;
280        }
281        if (!isset($microtime)) {
282            $microtime = microtime(true);
283        }
284        return round($microtime * 1000);
285    }
286
287    /**
288     * Gets the difference between a given microtime value and the starting time as milliseconds.
289     *
290     * @param float $microtime The microtime value - if not set the current time is used
291     * @return int The difference between a given microtime value and starting time as milliseconds
292     */
293    public function getDifferenceToStarttime($microtime = null)
294    {
295        return $this->getMilliseconds($microtime) - $this->starttime;
296    }
297
298    /**
299     * Get total parse time in milliseconds(without backend user initialization)
300     *
301     * @return int
302     */
303    public function getParseTime(): int
304    {
305        // Compensates for the time consumed with Back end user initialization.
306        $processStart = $this->getMilliseconds($GLOBALS['TYPO3_MISC']['microtime_start'] ?? null);
307
308        $beUserInitializationStart = $this->getMilliseconds($GLOBALS['TYPO3_MISC']['microtime_BE_USER_start'] ?? null);
309        $beUserInitializationEnd = $this->getMilliseconds($GLOBALS['TYPO3_MISC']['microtime_BE_USER_end'] ?? null);
310        $beUserInitialization = $beUserInitializationEnd - $beUserInitializationStart;
311
312        $processEnd = $this->getMilliseconds($GLOBALS['TYPO3_MISC']['microtime_end'] ?? null);
313        $totalParseTime = $processEnd - $processStart;
314
315        if ($beUserInitialization > 0) {
316            $totalParseTime -= $beUserInitialization;
317        }
318
319        return $totalParseTime;
320    }
321
322    /*******************************************
323     *
324     * Printing the parsing time information (for Admin Panel)
325     *
326     *******************************************/
327    /**
328     * Print TypoScript parsing log
329     *
330     * @return string HTML table with the information about parsing times.
331     */
332    public function printTSlog()
333    {
334        if (!$this->isEnabled) {
335            return '';
336        }
337        // Calculate times and keys for the tsStackLog
338        foreach ($this->tsStackLog as $uniqueId => &$data) {
339            $data['endtime'] = $this->getDifferenceToStarttime($data['endtime']);
340            $data['starttime'] = $this->getDifferenceToStarttime($data['starttime']);
341            $data['deltatime'] = $data['endtime'] - $data['starttime'];
342            if (is_array($data['tsStack'])) {
343                $data['key'] = implode($data['stackPointer'] ? '.' : '/', end($data['tsStack']));
344            }
345        }
346        unset($data);
347        // Create hierarchical array of keys pointing to the stack
348        $arr = [];
349        foreach ($this->tsStackLog as $uniqueId => $data) {
350            $this->createHierarchyArray($arr, $data['level'], $uniqueId);
351        }
352        // Parsing the registered content and create icon-html for the tree
353        $this->tsStackLog[$arr['0.'][0]]['content'] = $this->fixContent($arr['0.'], $this->tsStackLog[$arr['0.'][0]]['content'] ?? '', '', $arr['0.'][0]);
354        // Displaying the tree:
355        $outputArr = [];
356        $outputArr[] = $this->fw('TypoScript Key');
357        $outputArr[] = $this->fw('Value');
358        if ($this->printConf['allTime']) {
359            $outputArr[] = $this->fw('Time');
360            $outputArr[] = $this->fw('Own');
361            $outputArr[] = $this->fw('Sub');
362            $outputArr[] = $this->fw('Total');
363        } else {
364            $outputArr[] = $this->fw('Own');
365        }
366        $outputArr[] = $this->fw('Details');
367        $out = '';
368        foreach ($outputArr as $row) {
369            $out .= '<th>' . $row . '</th>';
370        }
371        $out = '<thead><tr>' . $out . '</tr></thead>';
372        $flag_tree = $this->printConf['flag_tree'];
373        $flag_messages = $this->printConf['flag_messages'];
374        $flag_content = $this->printConf['flag_content'];
375        $keyLgd = $this->printConf['keyLgd'];
376        $c = 0;
377        foreach ($this->tsStackLog as $uniqueId => $data) {
378            $logRowClass = '';
379            if ($this->highlightLongerThan && (int)$data['owntime'] > (int)$this->highlightLongerThan) {
380                $logRowClass = 'typo3-adminPanel-logRow-highlight';
381            }
382            $item = '';
383            // If first...
384            if (!$c) {
385                $data['icons'] = '';
386                $data['key'] = 'Script Start';
387                $data['value'] = '';
388            }
389            // Key label:
390            $keyLabel = '';
391            if (!$flag_tree && $data['stackPointer']) {
392                $temp = [];
393                foreach ($data['tsStack'] as $k => $v) {
394                    $temp[] = GeneralUtility::fixed_lgd_cs(implode($k ? '.' : '/', $v), -$keyLgd);
395                }
396                array_pop($temp);
397                $temp = array_reverse($temp);
398                array_pop($temp);
399                if (!empty($temp)) {
400                    $keyLabel = '<br /><span style="color:#999999;">' . implode('<br />', $temp) . '</span>';
401                }
402            }
403            if ($flag_tree) {
404                $tmp = GeneralUtility::trimExplode('.', $data['key'], true);
405                $theLabel = end($tmp);
406            } else {
407                $theLabel = $data['key'];
408            }
409            $theLabel = GeneralUtility::fixed_lgd_cs($theLabel, -$keyLgd);
410            $theLabel = $data['stackPointer'] ? '<span class="stackPointer">' . $theLabel . '</span>' : $theLabel;
411            $keyLabel = $theLabel . $keyLabel;
412            $item .= '<th scope="row" class="typo3-adminPanel-table-cell-key ' . $logRowClass . '">' . ($flag_tree ? $data['icons'] : '') . $this->fw($keyLabel) . '</th>';
413            // Key value:
414            $keyValue = $data['value'];
415            $item .= '<td class="' . $logRowClass . ' typo3-adminPanel-tsLogTime">' . $this->fw(htmlspecialchars($keyValue)) . '</td>';
416            if ($this->printConf['allTime']) {
417                $item .= '<td class="' . $logRowClass . ' typo3-adminPanel-tsLogTime"> ' . $this->fw($data['starttime']) . '</td>';
418                $item .= '<td class="' . $logRowClass . ' typo3-adminPanel-tsLogTime"> ' . $this->fw($data['owntime']) . '</td>';
419                $item .= '<td class="' . $logRowClass . ' typo3-adminPanel-tsLogTime"> ' . $this->fw(($data['subtime'] ? '+' . $data['subtime'] : '')) . '</td>';
420                $item .= '<td class="' . $logRowClass . ' typo3-adminPanel-tsLogTime"> ' . $this->fw(($data['subtime'] ? '=' . $data['deltatime'] : '')) . '</td>';
421            } else {
422                $item .= '<td class="' . $logRowClass . ' typo3-adminPanel-tsLogTime"> ' . $this->fw($data['owntime']) . '</td>';
423            }
424            // Messages:
425            $msgArr = [];
426            $msg = '';
427            if ($flag_messages && is_array($data['message'])) {
428                foreach ($data['message'] as $v) {
429                    $msgArr[] = nl2br($v);
430                }
431            }
432            if ($flag_content && (string)$data['content'] !== '') {
433                $maxlen = 120;
434                // Break lines which are too longer than $maxlen chars (can happen if content contains long paths...)
435                if (preg_match_all('/(\\S{' . $maxlen . ',})/', $data['content'], $reg)) {
436                    foreach ($reg[1] as $key => $match) {
437                        $match = preg_replace('/(.{' . $maxlen . '})/', '$1 ', $match);
438                        $data['content'] = str_replace($reg[0][$key], $match, $data['content']);
439                    }
440                }
441                $msgArr[] = nl2br($data['content']);
442            }
443            if (!empty($msgArr)) {
444                $msg = implode('<hr />', $msgArr);
445            }
446            $item .= '<td class="typo3-adminPanel-table-cell-content">' . $this->fw($msg) . '</td>';
447            $out .= '<tr>' . $item . '</tr>';
448            $c++;
449        }
450        $out = '<div class="typo3-adminPanel-table-overflow"><table class="typo3-adminPanel-table typo3-adminPanel-table-debug">' . $out . '</table></div>';
451        return $out;
452    }
453
454    /**
455     * Recursively generates the content to display
456     *
457     * @param array $arr Array which is modified with content. Reference
458     * @param string $content Current content string for the level
459     * @param string $depthData Prefixed icons for new PM icons
460     * @param string $vKey Seems to be the previous tsStackLog key
461     * @return string Returns the $content string generated/modified. Also the $arr array is modified!
462     */
463    protected function fixContent(&$arr, $content, $depthData = '', $vKey = '')
464    {
465        $entriesCount = 0;
466        $c = 0;
467        // First, find number of entries
468        foreach ($arr as $k => $v) {
469            //do not count subentries (the one ending with dot, eg. '9.'
470            if (\TYPO3\CMS\Core\Utility\MathUtility::canBeInterpretedAsInteger($k)) {
471                $entriesCount++;
472            }
473        }
474        // Traverse through entries
475        $subtime = 0;
476        foreach ($arr as $k => $v) {
477            if (\TYPO3\CMS\Core\Utility\MathUtility::canBeInterpretedAsInteger($k)) {
478                $c++;
479                $hasChildren = isset($arr[$k . '.']);
480                $lastEntry = $entriesCount === $c;
481
482                $PM = '<span class="treeline-icon treeline-icon-join' . ($lastEntry ? 'bottom' : '') . '"></span>';
483
484                $this->tsStackLog[$v]['icons'] = $depthData . $PM;
485                if ($this->tsStackLog[$v]['content'] !== '') {
486                    $content = str_replace($this->tsStackLog[$v]['content'], $v, $content);
487                }
488                if ($hasChildren) {
489                    $lineClass = $lastEntry ? 'treeline-icon-clear' : 'treeline-icon-line';
490                    $this->tsStackLog[$v]['content'] = $this->fixContent($arr[$k . '.'], $this->tsStackLog[$v]['content'], $depthData . '<span class="treeline-icon ' . $lineClass . '"></span>', $v);
491                } else {
492                    $this->tsStackLog[$v]['content'] = $this->fixCLen($this->tsStackLog[$v]['content'], $this->tsStackLog[$v]['value']);
493                    $this->tsStackLog[$v]['subtime'] = '';
494                    $this->tsStackLog[$v]['owntime'] = $this->tsStackLog[$v]['deltatime'];
495                }
496                $subtime += $this->tsStackLog[$v]['deltatime'];
497            }
498        }
499        // Set content with special chars
500        if (isset($this->tsStackLog[$vKey])) {
501            $this->tsStackLog[$vKey]['subtime'] = $subtime;
502            $this->tsStackLog[$vKey]['owntime'] = $this->tsStackLog[$vKey]['deltatime'] - $subtime;
503        }
504        $content = $this->fixCLen($content, $this->tsStackLog[$vKey]['value']);
505        // Traverse array again, this time substitute the unique hash with the red key
506        foreach ($arr as $k => $v) {
507            if (\TYPO3\CMS\Core\Utility\MathUtility::canBeInterpretedAsInteger($k)) {
508                if ($this->tsStackLog[$v]['content'] !== '') {
509                    $content = str_replace($v, '<strong style="color:red;">[' . $this->tsStackLog[$v]['key'] . ']</strong>', $content);
510                }
511            }
512        }
513        // Return the content
514        return $content;
515    }
516
517    /**
518     * Wraps the input content string in green colored span-tags IF the length o fthe input string exceeds $this->printConf['contentLength'] (or $this->printConf['contentLength_FILE'] if $v == "FILE"
519     *
520     * @param string $c The content string
521     * @param string $v Command: If "FILE" then $this->printConf['contentLength_FILE'] is used for content length comparison, otherwise $this->printConf['contentLength']
522     * @return string
523     */
524    protected function fixCLen($c, $v)
525    {
526        $len = $v === 'FILE' ? $this->printConf['contentLength_FILE'] : $this->printConf['contentLength'];
527        if (strlen($c) > $len) {
528            $c = '<span style="color:green;">' . htmlspecialchars(GeneralUtility::fixed_lgd_cs($c, $len)) . '</span>';
529        } else {
530            $c = htmlspecialchars($c);
531        }
532        return $c;
533    }
534
535    /**
536     * Wraps input string in a <span> tag
537     *
538     * @param string $str The string to be wrapped
539     * @return string
540     */
541    protected function fw($str)
542    {
543        return '<span>' . $str . '</span>';
544    }
545
546    /**
547     * Helper function for internal data manipulation
548     *
549     * @param array $arr Array (passed by reference) and modified
550     * @param int $pointer Pointer value
551     * @param string $uniqueId Unique ID string
552     * @internal
553     * @see printTSlog()
554     */
555    protected function createHierarchyArray(&$arr, $pointer, $uniqueId)
556    {
557        if (!is_array($arr)) {
558            $arr = [];
559        }
560        if ($pointer > 0) {
561            end($arr);
562            $k = key($arr);
563            $this->createHierarchyArray($arr[(int)$k . '.'], $pointer - 1, $uniqueId);
564        } else {
565            $arr[] = $uniqueId;
566        }
567    }
568}
569