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