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