1<?php 2 3/** 4 * Shortcut to ref, HTML mode 5 * 6 * @param mixed $args 7 * @return void|string 8 */ 9function r() 10{ 11 // arguments passed to this function 12 $args = func_get_args(); 13 14 // options (operators) gathered by the expression parser; 15 // this variable gets passed as reference to getInputExpressions(), which will store the operators in it 16 $options = array(); 17 18 // names of the arguments that were passed to this function 19 $expressions = ref::getInputExpressions($options); 20 $capture = in_array('@', $options, true); 21 22 // something went wrong while trying to parse the source expressions? 23 // if so, silently ignore this part and leave out the expression info 24 if(func_num_args() !== count($expressions)) 25 { 26 $expressions = null; 27 } 28 29 // use HTML formatter only if we're not in CLI mode, or if return was requested 30 $format = (php_sapi_name() !== 'cli') || $capture ? 'html' : 'cliText'; 31 32 // IE goes funky if there's no doctype 33 if(!$capture && ($format === 'html') && !headers_sent() && (!ob_get_level() || ini_get('output_buffering'))) 34 { 35 echo('<!DOCTYPE HTML><html><head><title>REF</title><meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /></head><body>'); 36 } 37 38 $ref = new ref($format); 39 40 // Observium specific paths 41 ref::config('stylePath', $GLOBALS['config']['html_dir'] . '/css/ref.css'); 42 ref::config('scriptPath', $GLOBALS['config']['html_dir'] . '/js/ref.js'); 43 44 if ($capture) 45 { 46 ob_start(); 47 } 48 49 foreach ($args as $index => $arg) 50 { 51 $ref->query($arg, $expressions ? $expressions[$index] : null); 52 } 53 54 // return the results if this function was called with the error suppression operator 55 if ($capture) 56 { 57 return ob_get_clean(); 58 } 59 60 // stop the script if this function was called with the bitwise not operator 61 if (in_array('~', $options, true) && ($format === 'html')) 62 { 63 echo('</body></html>'); 64 exit(0); 65 } 66} 67 68 69 70/** 71 * Shortcut to ref, plain text mode 72 * 73 * @param mixed $args 74 * @return void|string 75 */ 76function rt() 77{ 78 $args = func_get_args(); 79 $options = array(); 80 $output = ''; 81 $expressions = ref::getInputExpressions($options); 82 $capture = in_array('@', $options, true); 83 $ref = new ref((php_sapi_name() !== 'cli') || $capture ? 'text' : 'cliText'); 84 85 if (func_num_args() !== count($expressions)) 86 { 87 $expressions = null; 88 } 89 90 if (!headers_sent()) 91 { 92 header('Content-Type: text/plain; charset=utf-8'); 93 } 94 95 if ($capture) 96 { 97 ob_start(); 98 } 99 100 foreach ($args as $index => $arg) 101 { 102 $ref->query($arg, $expressions ? $expressions[$index] : null); 103 } 104 105 if ($capture) 106 { 107 return ob_get_clean(); 108 } 109 110 if (in_array('~', $options, true)) 111 { 112 exit(0); 113 } 114} 115 116 117/** 118 * REF is a nicer alternative to PHP's print_r() / var_dump(). 119 * 120 * @version 1.0 121 * @author digitalnature - http://digitalnature.eu 122 */ 123class ref 124{ 125 126 const 127 128 MARKER_KEY = '_phpRefArrayMarker_'; 129 130 131 protected static 132 133 /** 134 * CPU time used for processing 135 * 136 * @var array 137 */ 138 $time = 0, 139 140 /** 141 * Configuration (+ default values) 142 * 143 * @var array 144 */ 145 $config = array( 146 147 // initially expanded levels (for HTML mode only) 148 'expLvl' => 1, 149 150 // depth limit (0 = no limit); 151 // this is not related to recursion 152 'maxDepth' => 6, 153 154 // show the place where r() has been called from 155 'showBacktrace' => true, 156 157 // if passed from high level function -> debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS) 158 'Backtrace' => NULL, 159 160 // display iterator contents 161 'showIteratorContents' => false, 162 163 // display extra information about resources 164 'showResourceInfo' => true, 165 166 // display method and parameter list on objects 167 'showMethods' => true, 168 169 // display private properties / methods 170 'showPrivateMembers' => false, 171 172 // peform string matches (date, file, functions, classes, json, serialized data, regex etc.) 173 // note: seriously slows down queries on large amounts of data 174 'showStringMatches' => true, 175 176 // shortcut functions used to access the query method below; 177 // if they are namespaced, the namespace must be present as well (methods are not supported) 178 'shortcutFunc' => array('r', 'rt'), 179 180 // custom/external formatters (as associative array: format => className) 181 'formatters' => array(), 182 183 // stylesheet path (for HTML only); 184 // 'false' means no styles 185 'stylePath' => '{:dir}/ref.css', 186 187 // javascript path (for HTML only); 188 // 'false' means no js 189 'scriptPath' => '{:dir}/ref.js', 190 191 // display url info via cURL 192 'showUrls' => false, 193 194 // stop evaluation after this amount of time (seconds) 195 'timeout' => 10, 196 197 // whether to produce W3c-valid HTML, 198 // or unintelligible, but optimized markup that takes less space 199 'validHtml' => false, 200 ), 201 202 /** 203 * Some environment variables 204 * used to determine feature support 205 * 206 * @var array 207 */ 208 $env = array(), 209 210 /** 211 * Timeout point 212 * 213 * @var bool 214 */ 215 $timeout = -1, 216 217 $debug = array( 218 'cacheHits' => 0, 219 'objects' => 0, 220 'arrays' => 0, 221 'scalars' => 0, 222 ); 223 224 225 protected 226 227 /** 228 * Output formatter of this instance 229 * 230 * @var RFormatter 231 */ 232 $fmt = null, 233 234 /** 235 * Start time of the current instance 236 * 237 * @var float 238 */ 239 $startTime = 0, 240 241 /** 242 * Internally created objects 243 * 244 * @var SplObjectStorage 245 */ 246 $intObjects = null; 247 248 249 /** 250 * Constructor 251 * 252 * @param string|RFormatter $format Output format ID, or formatter instance defaults to 'html' 253 */ 254 public function __construct($format = 'html') 255 { 256 257 static $didIni = false; 258 259 if (!$didIni) 260 { 261 $didIni = true; 262 foreach (array_keys(static::$config) as $key) 263 { 264 $iniVal = get_cfg_var('ref.' . $key); 265 if ($iniVal !== false) 266 { 267 static::$config[$key] = $iniVal; 268 } 269 } 270 271 } 272 273 if ($format instanceof RFormatter) 274 { 275 $this->fmt = $format; 276 277 } else { 278 $format = isset(static::$config['formatters'][$format]) ? static::$config['formatters'][$format] : 'R' . ucfirst($format) . 'Formatter'; 279 280 if (!class_exists($format, false)) 281 { 282 throw new \Exception(sprintf('%s class not found', $format)); 283 } 284 285 $this->fmt = new $format(); 286 } 287 288 if (static::$env) 289 { 290 return; 291 } 292 293 static::$env = array( 294 295 // php 5.4+ ? 296 'is54' => version_compare(PHP_VERSION, '5.4') >= 0, 297 298 // php 5.4.6+ ? 299 'is546' => version_compare(PHP_VERSION, '5.4.6') >= 0, 300 301 // php 5.6+ 302 'is56' => version_compare(PHP_VERSION, '5.6') >= 0, 303 304 // php 7.0+ ? 305 'is7' => version_compare(PHP_VERSION, '7.0') >= 0, 306 307 // curl extension running? 308 'curlActive' => function_exists('curl_version'), 309 310 // is the 'mbstring' extension active? 311 'mbStr' => function_exists('mb_detect_encoding'), 312 313 // @see: https://bugs.php.net/bug.php?id=52469 314 'supportsDate' => (strncasecmp(PHP_OS, 'WIN', 3) !== 0) || (version_compare(PHP_VERSION, '5.3.10') >= 0), 315 ); 316 } 317 318 319 /** 320 * Enforce proper use of this class 321 * 322 * @param string $name 323 */ 324 public function __get($name) 325 { 326 throw new \Exception(sprintf('No such property: %s', $name)); 327 } 328 329 330 /** 331 * Enforce proper use of this class 332 * 333 * @param string $name 334 * @param mixed $value 335 */ 336 public function __set($name, $value) 337 { 338 throw new \Exception(sprintf('Cannot set %s. Not allowed', $name)); 339 } 340 341 342 /** 343 * Generate structured information about a variable/value/expression (subject) 344 * 345 * Output is flushed to the screen 346 * 347 * @param mixed $subject 348 * @param string $expression 349 */ 350 public function query($subject, $expression = null) 351 { 352 if (static::$timeout > 0) 353 { 354 return; 355 } 356 357 $this->startTime = microtime(true); 358 359 $this->intObjects = new \SplObjectStorage(); 360 361 $this->fmt->startRoot(); 362 $this->fmt->startExp(); 363 $this->evaluateExp($expression); 364 $this->fmt->endExp(); 365 $this->evaluate($subject); 366 $this->fmt->endRoot(); 367 $this->fmt->flush(); 368 369 static::$time += microtime(true) - $this->startTime; 370 } 371 372 373 /** 374 * Executes a function the given number of times and returns the elapsed time. 375 * 376 * Keep in mind that the returned time includes function call overhead (including 377 * microtime calls) x iteration count. This is why this is better suited for 378 * determining which of two or more functions is the fastest, rather than 379 * finding out how fast is a single function. 380 * 381 * @param int $iterations Number of times the function will be executed 382 * @param callable $function Function to execute 383 * @param mixed &$output If given, last return value will be available in this variable 384 * @return double Elapsed time 385 */ 386 public static function timeFunc($iterations, $function, &$output = null) 387 { 388 389 $time = 0; 390 391 for ($i = 0; $i < $iterations; $i++) 392 { 393 $start = microtime(true); 394 $output = call_user_func($function); 395 $time += microtime(true) - $start; 396 } 397 398 return round($time, 4); 399 } 400 401 402 403 /** 404 * Timer utility 405 * 406 * First call of this function will start the timer. 407 * The second call will stop the timer and return the elapsed time 408 * since the timer started. 409 * 410 * Multiple timers can be controlled simultaneously by specifying a timer ID. 411 * 412 * @since 1.0 413 * @param int $id Timer ID, optional 414 * @param int $precision Precision of the result, optional 415 * @return void|double Elapsed time, or void if the timer was just started 416 */ 417 public static function timer($id = 1, $precision = 4) 418 { 419 420 static 421 $timers = array(); 422 423 // check if this timer was started, and display the elapsed time if so 424 if (isset($timers[$id])) 425 { 426 $elapsed = round(microtime(true) - $timers[$id], $precision); 427 unset($timers[$id]); 428 return $elapsed; 429 } 430 431 // ID doesn't exist, start new timer 432 $timers[$id] = microtime(true); 433 } 434 435 436 /** 437 * Parses a DocBlock comment into a data structure. 438 * 439 * @link http://pear.php.net/manual/en/standards.sample.php 440 * @param string $comment DocBlock comment (must start with /**) 441 * @param string|null $key Field to return (optional) 442 * @return array|string|null Array containing all fields, array/string with the contents of 443 * the requested field, or null if the comment is empty/invalid 444 */ 445 public static function parseComment($comment, $key = null) 446 { 447 448 $description = ''; 449 $tags = array(); 450 $tag = null; 451 $pointer = ''; 452 $padding = 0; 453 $comment = preg_split('/\r\n|\r|\n/', '* ' . trim($comment, "/* \t\n\r\0\x0B")); 454 455 // analyze each line 456 foreach ($comment as $line) 457 { 458 459 // drop any wrapping spaces 460 $line = trim($line); 461 462 // drop "* " 463 if ($line !== '') 464 { 465 $line = substr($line, 2); 466 } 467 468 if (strpos($line, '@') !== 0) 469 { 470 471 // preserve formatting of tag descriptions, 472 // because they may span across multiple lines 473 if ($tag !== null) 474 { 475 $trimmed = trim($line); 476 477 if ($padding !== 0) 478 { 479 $trimmed = static::strPad($trimmed, static::strLen($line) - $padding, ' ', STR_PAD_LEFT); 480 } else { 481 $padding = static::strLen($line) - static::strLen($trimmed); 482 } 483 484 $pointer .= "\n{$trimmed}"; 485 continue; 486 } 487 488 // tag definitions have not started yet; assume this is part of the description text 489 $description .= "\n{$line}"; 490 continue; 491 } 492 493 $padding = 0; 494 $parts = explode(' ', $line, 2); 495 496 // invalid tag? (should we include it as an empty array?) 497 if (!isset($parts[1])) 498 { 499 continue; 500 } 501 502 $tag = substr($parts[0], 1); 503 $line = ltrim($parts[1]); 504 505 // tags that have a single component (eg. link, license, author, throws...); 506 // note that @throws may have 2 components, however most people use it like "@throws ExceptionClass if whatever...", 507 // which, if broken into two values, leads to an inconsistent description sentence 508 if (!in_array($tag, array('global', 'param', 'return', 'var'))) 509 { 510 $tags[$tag][] = $line; 511 end($tags[$tag]); 512 $pointer = &$tags[$tag][key($tags[$tag])]; 513 continue; 514 } 515 516 // tags with 2 or 3 components (var, param, return); 517 $parts = explode(' ', $line, 2); 518 $parts[1] = isset($parts[1]) ? ltrim($parts[1]) : null; 519 $lastIdx = 1; 520 521 // expecting 3 components on the 'param' tag: type varName varDescription 522 if ($tag === 'param') 523 { 524 $lastIdx = 2; 525 if (in_array($parts[1][0], array('&', '$'), true)) 526 { 527 $line = ltrim(array_pop($parts)); 528 $parts = array_merge($parts, explode(' ', $line, 2)); 529 $parts[2] = isset($parts[2]) ? ltrim($parts[2]) : null; 530 } else { 531 $parts[2] = $parts[1]; 532 $parts[1] = null; 533 } 534 } 535 536 $tags[$tag][] = $parts; 537 end($tags[$tag]); 538 $pointer = &$tags[$tag][key($tags[$tag])][$lastIdx]; 539 } 540 541 // split title from the description texts at the nearest 2x new-line combination 542 // (note: loose check because 0 isn't valid as well) 543 if (strpos($description, "\n\n")) 544 { 545 list($title, $description) = explode("\n\n", $description, 2); 546 547 // if we don't have 2 new lines, try to extract first sentence 548 } else { 549 // in order for a sentence to be considered valid, 550 // the next one must start with an uppercase letter 551 $sentences = preg_split('/(?<=[.?!])\s+(?=[A-Z])/', $description, 2, PREG_SPLIT_NO_EMPTY); 552 553 // failed to detect a second sentence? then assume there's only title and no description text 554 $title = isset($sentences[0]) ? $sentences[0] : $description; 555 $description = isset($sentences[1]) ? $sentences[1] : ''; 556 } 557 558 $title = ltrim($title); 559 $description = ltrim($description); 560 561 $data = compact('title', 'description', 'tags'); 562 563 if (!array_filter($data)) 564 { 565 return null; 566 } 567 568 if ($key !== null) 569 { 570 return isset($data[$key]) ? $data[$key] : null; 571 } 572 573 return $data; 574 } 575 576 577 /** 578 * Split a regex into its components 579 * 580 * Based on "Regex Colorizer" by Steven Levithan (this is a translation from javascript) 581 * 582 * @link https://github.com/slevithan/regex-colorizer 583 * @link https://github.com/symfony/Finder/blob/master/Expression/Regex.php#L64-74 584 * @param string $pattern 585 * @return array 586 */ 587 public static function splitRegex($pattern) 588 { 589 590 // detection attempt code from the Symfony Finder component 591 $maybeValid = false; 592 if (preg_match('/^(.{3,}?)([imsxuADU]*)$/', $pattern, $m)) 593 { 594 $start = substr($m[1], 0, 1); 595 $end = substr($m[1], -1); 596 597 if (($start === $end && !preg_match('/[*?[:alnum:] \\\\]/', $start)) || ($start === '{' && $end === '}')) 598 { 599 $maybeValid = true; 600 } 601 } 602 603 if (!$maybeValid) 604 { 605 throw new \Exception('Pattern does not appear to be a valid PHP regex'); 606 } 607 608 $output = array(); 609 $capturingGroupCount = 0; 610 $groupStyleDepth = 0; 611 $openGroups = array(); 612 $lastIsQuant = false; 613 $lastType = 1; // 1 = none; 2 = alternator 614 $lastStyle = null; 615 616 preg_match_all('/\[\^?]?(?:[^\\\\\]]+|\\\\[\S\s]?)*]?|\\\\(?:0(?:[0-3][0-7]{0,2}|[4-7][0-7]?)?|[1-9][0-9]*|x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4}|c[A-Za-z]|[\S\s]?)|\((?:\?[:=!]?)?|(?:[?*+]|\{[0-9]+(?:,[0-9]*)?\})\??|[^.?*+^${[()|\\\\]+|./', $pattern, $matches); 617 618 $matches = $matches[0]; 619 620 $getTokenCharCode = function($token) 621 { 622 if (strlen($token) > 1 && $token[0] === '\\') 623 { 624 $t1 = substr($token, 1); 625 626 if (preg_match('/^c[A-Za-z]$/', $t1)) 627 { 628 return strpos("ABCDEFGHIJKLMNOPQRSTUVWXYZ", strtoupper($t1[1])) + 1; 629 } 630 631 if (preg_match('/^(?:x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4})$/', $t1)) 632 { 633 return intval(substr($t1, 1), 16); 634 } 635 636 if (preg_match('/^(?:[0-3][0-7]{0,2}|[4-7][0-7]?)$/', $t1)) 637 { 638 return intval($t1, 8); 639 } 640 641 $len = strlen($t1); 642 643 if ($len === 1 && strpos('cuxDdSsWw', $t1) !== false) 644 { 645 return null; 646 } 647 648 if ($len === 1) 649 { 650 switch ($t1) 651 { 652 case 'b': return 8; 653 case 'f': return 12; 654 case 'n': return 10; 655 case 'r': return 13; 656 case 't': return 9; 657 case 'v': return 11; 658 default: return $t1[0]; 659 } 660 } 661 } 662 663 return ($token !== '\\') ? $token[0] : null; 664 }; 665 666 foreach ($matches as $m) 667 { 668 669 if ($m[0] === '[') 670 { 671 $lastCC = null; 672 $cLastRangeable = false; 673 $cLastType = 0; // 0 = none; 1 = range hyphen; 2 = short class 674 675 preg_match('/^(\[\^?)(]?(?:[^\\\\\]]+|\\\\[\S\s]?)*)(]?)$/', $m, $parts); 676 677 array_shift($parts); 678 list($opening, $content, $closing) = $parts; 679 680 if (!$closing) 681 { 682 throw new \Exception('Unclosed character class'); 683 } 684 685 preg_match_all('/[^\\\\-]+|-|\\\\(?:[0-3][0-7]{0,2}|[4-7][0-7]?|x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4}|c[A-Za-z]|[\S\s]?)/', $content, $ccTokens); 686 $ccTokens = $ccTokens[0]; 687 $ccTokenCount = count($ccTokens); 688 $output[] = array('chr' => $opening); 689 690 foreach($ccTokens as $i => $cm) 691 { 692 693 if ($cm[0] === '\\') 694 { 695 if (preg_match('/^\\\\[cux]$/', $cm)) 696 { 697 throw new \Exception('Incomplete regex token'); 698 } 699 700 if (preg_match('/^\\\\[dsw]$/i', $cm)) 701 { 702 $output[] = array('chr-meta' => $cm); 703 $cLastRangeable = ($cLastType !== 1); 704 $cLastType = 2; 705 706 } 707 else if ($cm === '\\') 708 { 709 throw new \Exception('Incomplete regex token'); 710 711 } else { 712 $output[] = array('chr-meta' => $cm); 713 $cLastRangeable = $cLastType !== 1; 714 $lastCC = $getTokenCharCode($cm); 715 } 716 717 } 718 else if($cm === '-') 719 { 720 if ($cLastRangeable) 721 { 722 $nextToken = ($i + 1 < $ccTokenCount) ? $ccTokens[$i + 1] : false; 723 724 if ($nextToken) 725 { 726 $nextTokenCharCode = $getTokenCharCode($nextToken[0]); 727 728 if ((!is_null($nextTokenCharCode) && $lastCC > $nextTokenCharCode) || $cLastType === 2 || preg_match('/^\\\\[dsw]$/i', $nextToken[0])) 729 { 730 throw new \Exception('Reversed or invalid range'); 731 } 732 733 $output[] = array('chr-range' => '-'); 734 $cLastRangeable = false; 735 $cLastType = 1; 736 737 } else { 738 $output[] = $closing ? array('chr' => '-') : array('chr-range' => '-'); 739 } 740 741 } else { 742 $output[] = array('chr' => '-'); 743 $cLastRangeable = ($cLastType !== 1); 744 } 745 746 } else { 747 $output[] = array('chr' => $cm); 748 $cLastRangeable = strlen($cm) > 1 || ($cLastType !== 1); 749 $lastCC = $cm[strlen($cm) - 1]; 750 } 751 } 752 753 $output[] = array('chr' => $closing); 754 $lastIsQuant = true; 755 756 } 757 else if ($m[0] === '(') 758 { 759 if (strlen($m) === 2) 760 { 761 throw new \Exception('Invalid or unsupported group type'); 762 } 763 764 if (strlen($m) === 1) 765 { 766 $capturingGroupCount++; 767 } 768 769 $groupStyleDepth = ($groupStyleDepth !== 5) ? $groupStyleDepth + 1 : 1; 770 $openGroups[] = $m; // opening 771 $lastIsQuant = false; 772 $output[] = array("g{$groupStyleDepth}" => $m); 773 774 } 775 else if ($m[0] === ')') 776 { 777 if (!count($openGroups)) 778 { 779 throw new \Exception('No matching opening parenthesis'); 780 } 781 782 $output[] = array('g' . $groupStyleDepth => ')'); 783 $prevGroup = $openGroups[count($openGroups) - 1]; 784 $prevGroup = isset($prevGroup[2]) ? $prevGroup[2] : ''; 785 $lastIsQuant = !preg_match('/^[=!]/', $prevGroup); 786 $lastStyle = "g{$groupStyleDepth}"; 787 $lastType = 0; 788 $groupStyleDepth = ($groupStyleDepth !== 1) ? $groupStyleDepth - 1 : 5; 789 790 array_pop($openGroups); 791 continue; 792 793 } 794 else if ($m[0] === '\\') 795 { 796 if (isset($m[1]) && preg_match('/^[1-9]/', $m[1])) 797 { 798 $nonBackrefDigits = ''; 799 $num = substr(+$m, 1); 800 801 while ($num > $capturingGroupCount) 802 { 803 preg_match('/[0-9]$/', $num, $digits); 804 $nonBackrefDigits = $digits[0] . $nonBackrefDigits; 805 $num = floor($num / 10); 806 } 807 808 if ($num > 0) 809 { 810 $output[] = array('meta' => "\\{$num}", 'text' => $nonBackrefDigits); 811 812 } else { 813 preg_match('/^\\\\([0-3][0-7]{0,2}|[4-7][0-7]?|[89])([0-9]*)/', $m, $pts); 814 $output[] = array('meta' => '\\' . $pts[1], 'text' => $pts[2]); 815 } 816 817 $lastIsQuant = true; 818 819 } 820 else if (isset($m[1]) && preg_match('/^[0bBcdDfnrsStuvwWx]/', $m[1])) 821 { 822 823 if (preg_match('/^\\\\[cux]$/', $m)) 824 { 825 throw new \Exception('Incomplete regex token'); 826 } 827 828 $output[] = array('meta' => $m); 829 $lastIsQuant = (strpos('bB', $m[1]) === false); 830 831 } 832 else if ($m === '\\') 833 { 834 throw new \Exception('Incomplete regex token'); 835 836 } else { 837 $output[] = array('text' => $m); 838 $lastIsQuant = true; 839 } 840 841 } 842 else if (preg_match('/^(?:[?*+]|\{[0-9]+(?:,[0-9]*)?\})\??$/', $m)) 843 { 844 if (!$lastIsQuant) 845 { 846 throw new \Exception('Quantifiers must be preceded by a token that can be repeated'); 847 } 848 849 preg_match('/^\{([0-9]+)(?:,([0-9]*))?/', $m, $interval); 850 851 if ($interval && (+$interval[1] > 65535 || (isset($interval[2]) && (+$interval[2] > 65535)))) 852 { 853 throw new \Exception('Interval quantifier cannot use value over 65,535'); 854 } 855 856 if ($interval && isset($interval[2]) && (+$interval[1] > +$interval[2])) 857 { 858 throw new \Exception('Interval quantifier range is reversed'); 859 } 860 861 $output[] = array($lastStyle ? $lastStyle : 'meta' => $m); 862 $lastIsQuant = false; 863 864 } 865 else if ($m === '|') 866 { 867 if ($lastType === 1 || ($lastType === 2 && !count($openGroups))) 868 { 869 throw new \Exception('Empty alternative effectively truncates the regex here'); 870 } 871 872 $output[] = count($openGroups) ? array("g{$groupStyleDepth}" => '|') : array('meta' => '|'); 873 $lastIsQuant = false; 874 $lastType = 2; 875 $lastStyle = ''; 876 continue; 877 878 } 879 else if ($m === '^' || $m === '$') 880 { 881 $output[] = array('meta' => $m); 882 $lastIsQuant = false; 883 884 } 885 else if ($m === '.') 886 { 887 $output[] = array('meta' => '.'); 888 $lastIsQuant = true; 889 890 } else { 891 $output[] = array('text' => $m); 892 $lastIsQuant = true; 893 } 894 895 $lastType = 0; 896 $lastStyle = ''; 897 } 898 899 if ($openGroups) 900 { 901 throw new \Exception('Unclosed grouping'); 902 } 903 904 return $output; 905 } 906 907 908 /** 909 * Set or get configuration options 910 * 911 * @param string $key 912 * @param mixed|null $value 913 * @return mixed 914 */ 915 public static function config($key, $value = null) 916 { 917 918 if (!array_key_exists($key, static::$config)) 919 { 920 throw new \Exception(sprintf('Unrecognized option: "%s". Valid options are: %s', $key, implode(', ', array_keys(static::$config)))); 921 } 922 923 if ($value === null) 924 { 925 return static::$config[$key]; 926 } 927 928 if (is_array(static::$config[$key])) 929 { 930 return static::$config[$key] = (array)$value; 931 } 932 933 return static::$config[$key] = $value; 934 } 935 936 937 /** 938 * Total CPU time used by the class 939 * 940 * @param int precision 941 * @return double 942 */ 943 public static function getTime($precision = 4) 944 { 945 return round(static::$time, $precision); 946 } 947 948 949 /** 950 * Get relevant backtrace info for last ref call 951 * 952 * @return array|false 953 */ 954 public static function getBacktrace() 955 { 956 957 if (ref::config('showBacktrace')) 958 { 959 // pull only basic info with php 5.3.6+ to save some memory 960 if (NULL !== ref::config('Backtrace')) 961 { 962 // Observium hack for get original backtrace 963 $trace = ref::config('Backtrace'); 964 } else { 965 $trace = defined('DEBUG_BACKTRACE_IGNORE_ARGS') ? debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS) : debug_backtrace(); 966 } 967 } 968 969 while ($callee = array_pop($trace)) 970 { 971 972 // extract only the information we need 973 $callee = array_intersect_key($callee, array_fill_keys(array('file', 'function', 'line'), false)); 974 extract($callee, EXTR_OVERWRITE); 975 976 // skip, if the called function doesn't match the shortcut function name 977 if (!$function || !in_array(strtolower((string)$function), static::$config['shortcutFunc'])) 978 { 979 continue; 980 } 981 982 return compact('file', 'function', 'line'); 983 } 984 985 return false; 986 } 987 988 989 /** 990 * Determines the input expression(s) passed to the shortcut function 991 * 992 * @param array &$options Optional, options to gather (from operators) 993 * @return array Array of string expressions 994 */ 995 public static function getInputExpressions(array &$options = null) 996 { 997 998 // used to determine the position of the current call, 999 // if more queries calls were made on the same line 1000 static $lineInst = array(); 1001 1002 $trace = static::getBacktrace(); 1003 1004 if (!$trace) 1005 { 1006 return array(); 1007 } 1008 1009 extract($trace); 1010 1011 $code = file($file); 1012 $code = $code[$line - 1]; // multiline expressions not supported! 1013 $instIndx = 0; 1014 $tokens = token_get_all("<?php {$code}"); 1015 1016 // locate the caller position in the line, and isolate argument tokens 1017 foreach ($tokens as $i => $token) 1018 { 1019 1020 // match token with our shortcut function name 1021 if (is_string($token) || ($token[0] !== T_STRING) || (strcasecmp($token[1], $function) !== 0)) 1022 { 1023 continue; 1024 } 1025 1026 // is this some method that happens to have the same name as the shortcut function? 1027 if (isset($tokens[$i - 1]) && is_array($tokens[$i - 1]) && in_array($tokens[$i - 1][0], array(T_DOUBLE_COLON, T_OBJECT_OPERATOR), true)) 1028 { 1029 continue; 1030 } 1031 1032 // find argument definition start, just after '(' 1033 if (isset($tokens[$i + 1]) && ($tokens[$i + 1][0] === '(')) 1034 { 1035 $instIndx++; 1036 1037 if (!isset($lineInst[$line])) 1038 { 1039 $lineInst[$line] = 0; 1040 } 1041 1042 if ($instIndx <= $lineInst[$line]) 1043 { 1044 continue; 1045 } 1046 1047 $lineInst[$line]++; 1048 1049 // gather options 1050 if ($options !== null) 1051 { 1052 $j = $i - 1; 1053 while (isset($tokens[$j]) && is_string($tokens[$j]) && in_array($tokens[$j], array('@', '+', '-', '!', '~'))) 1054 { 1055 $options[] = $tokens[$j--]; 1056 } 1057 } 1058 1059 $lvl = $index = $curlies = 0; 1060 $expressions = array(); 1061 1062 // get the expressions 1063 foreach (array_slice($tokens, $i + 2) as $token) 1064 { 1065 1066 if (is_array($token)) 1067 { 1068 if ($token[0] !== T_COMMENT) 1069 { 1070 $expressions[$index][] = ($token[0] !== T_WHITESPACE) ? $token[1] : ' '; 1071 } 1072 1073 continue; 1074 } 1075 1076 if ($token === '{') 1077 { 1078 $curlies++; 1079 } 1080 1081 if ($token === '}') 1082 { 1083 $curlies--; 1084 } 1085 1086 if ($token === '(') 1087 { 1088 $lvl++; 1089 } 1090 1091 if ($token === ')') 1092 { 1093 $lvl--; 1094 } 1095 1096 // assume next argument if a comma was encountered, 1097 // and we're not insde a curly bracket or inner parentheses 1098 if (($curlies < 1) && ($lvl === 0) && ($token === ',')) 1099 { 1100 $index++; 1101 continue; 1102 } 1103 1104 // negative parentheses count means we reached the end of argument definitions 1105 if ($lvl < 0) 1106 { 1107 foreach($expressions as &$expression) 1108 { 1109 $expression = trim(implode('', $expression)); 1110 } 1111 1112 return $expressions; 1113 } 1114 1115 $expressions[$index][] = $token; 1116 } 1117 1118 break; 1119 } 1120 } 1121 1122 } 1123 1124 1125 /** 1126 * Get all parent classes of a class 1127 * 1128 * @param Reflector $class Reflection object 1129 * @return array Array of ReflectionClass objects (starts with the ancestor, ends with the given class) 1130 */ 1131 protected static function getParentClasses(\Reflector $class) 1132 { 1133 1134 $parents = array($class); 1135 while (($class = $class->getParentClass()) !== false) 1136 { 1137 $parents[] = $class; 1138 } 1139 1140 return array_reverse($parents); 1141 } 1142 1143 1144 1145 /** 1146 * Generate class / function info 1147 * 1148 * @param Reflector $reflector Class name or reflection object 1149 * @param string $single Skip parent classes 1150 * @param Reflector|null $context Object context (for methods) 1151 * @return string 1152 */ 1153 protected function fromReflector(\Reflector $reflector, $single = '', \Reflector $context = null) 1154 { 1155 1156 // @todo: test this 1157 $hash = var_export(func_get_args(), true); 1158 //$hash = $reflector->getName() . ';' . $single . ';' . ($context ? $context->getName() : ''); 1159 1160 if ($this->fmt->didCache($hash)) 1161 { 1162 static::$debug['cacheHits']++; 1163 return; 1164 } 1165 1166 $items = array($reflector); 1167 1168 if (($single === '') && ($reflector instanceof \ReflectionClass)) 1169 { 1170 $items = static::getParentClasses($reflector); 1171 } 1172 1173 $first = true; 1174 foreach ($items as $item) 1175 { 1176 1177 if (!$first) 1178 { 1179 $this->fmt->sep(' :: '); 1180 } 1181 1182 $first = false; 1183 $name = ($single !== '') ? $single : $item->getName(); 1184 $comments = $item->isInternal() ? array() : static::parseComment($item->getDocComment()); 1185 $meta = array('sub' => array()); 1186 $bubbles = array(); 1187 1188 if ($item->isInternal()) 1189 { 1190 $extension = $item->getExtension(); 1191 $meta['title'] = ($extension instanceof \ReflectionExtension) ? sprintf('Internal - part of %s (%s)', $extension->getName(), $extension->getVersion()) : 'Internal'; 1192 1193 } else { 1194 $comments = static::parseComment($item->getDocComment()); 1195 1196 if ($comments) 1197 { 1198 $meta += $comments; 1199 } 1200 1201 $meta['sub'][] = array('Defined in', basename($item->getFileName()) . ':' . $item->getStartLine()); 1202 } 1203 1204 if (($item instanceof \ReflectionFunction) || ($item instanceof \ReflectionMethod)) 1205 { 1206 if (($context !== null) && ($context->getShortName() !== $item->getDeclaringClass()->getShortName())) 1207 { 1208 $meta['sub'][] = array('Inherited from', $item->getDeclaringClass()->getShortName()); 1209 } 1210 1211 // @note: PHP 7 seems to crash when calling getPrototype on Closure::__invoke() 1212 if (($item instanceof \ReflectionMethod) && !$item->isInternal()) 1213 { 1214 try 1215 { 1216 $proto = $item->getPrototype(); 1217 $meta['sub'][] = array('Prototype defined by', $proto->class); 1218 } catch(\Exception $e) {} 1219 } 1220 1221 $this->fmt->text('name', $name, $meta, $this->linkify($item)); 1222 continue; 1223 } 1224 1225 // @todo: maybe - list interface methods 1226 if (!($item->isInterface() || (static::$env['is54'] && $item->isTrait()))) 1227 { 1228 1229 if ($item->isAbstract()) 1230 { 1231 $bubbles[] = array('A', 'Abstract'); 1232 } 1233 1234 if (static::$env['is7'] && $item->isAnonymous()) 1235 { 1236 $bubbles[] = array('?', 'Anonymous'); 1237 } 1238 1239 if ($item->isFinal()) 1240 { 1241 $bubbles[] = array('F', 'Final'); 1242 } 1243 1244 // php 5.4+ only 1245 if (static::$env['is54'] && $item->isCloneable()) 1246 { 1247 $bubbles[] = array('C', 'Cloneable'); 1248 } 1249 1250 if ($item->isIterateable()) 1251 { 1252 $bubbles[] = array('X', 'Iterateable'); 1253 } 1254 1255 } 1256 1257 if ($item->isInterface() && $single !== '') 1258 { 1259 $bubbles[] = array('I', 'Interface'); 1260 } 1261 1262 if ($bubbles) 1263 { 1264 $this->fmt->bubbles($bubbles); 1265 } 1266 1267 if ($item->isInterface() && $single === '') 1268 { 1269 $name .= sprintf(' (%d)', count($item->getMethods())); 1270 } 1271 1272 $this->fmt->text('name', $name, $meta, $this->linkify($item)); 1273 } 1274 1275 $this->fmt->cacheLock($hash); 1276 } 1277 1278 1279 /** 1280 * Generates an URL that points to the documentation page relevant for the requested context 1281 * 1282 * For internal functions and classes, the URI will point to the local PHP manual 1283 * if installed and configured, otherwise to php.net/manual (the english one) 1284 * 1285 * @param Reflector $reflector Reflector object (used to determine the URL scheme for internal stuff) 1286 * @param string|null $constant Constant name, if this is a request to linkify a constant 1287 * @return string|null URL 1288 */ 1289 protected function linkify(\Reflector $reflector, $constant = null) 1290 { 1291 1292 static $docRefRoot = null, $docRefExt = null; 1293 1294 // most people don't have this set 1295 if (!$docRefRoot) 1296 { 1297 $docRefRoot = ($docRefRoot = rtrim(ini_get('docref_root'), '/')) ? $docRefRoot : 'http://php.net/manual/en'; 1298 } 1299 1300 if (!$docRefExt) 1301 { 1302 $docRefExt = ($docRefExt = ini_get('docref_ext')) ? $docRefExt : '.php'; 1303 } 1304 1305 $phpNetSchemes = array( 1306 'class' => $docRefRoot . '/class.%s' . $docRefExt, 1307 'function' => $docRefRoot . '/function.%s' . $docRefExt, 1308 'method' => $docRefRoot . '/%2$s.%1$s' . $docRefExt, 1309 'property' => $docRefRoot . '/class.%2$s' . $docRefExt . '#%2$s.props.%1$s', 1310 'constant' => $docRefRoot . '/class.%2$s' . $docRefExt . '#%2$s.constants.%1$s', 1311 ); 1312 1313 $url = null; 1314 $args = array(); 1315 1316 // determine scheme 1317 if ($constant !== null) 1318 { 1319 $type = 'constant'; 1320 $args[] = $constant; 1321 1322 } else { 1323 $type = explode('\\', get_class($reflector)); 1324 $type = strtolower(ltrim(end($type), 'Reflection')); 1325 1326 if ($type === 'object') 1327 { 1328 $type = 'class'; 1329 } 1330 } 1331 1332 // properties don't have the internal flag; 1333 // also note that many internal classes use some kind of magic as properties (eg. DateTime); 1334 // these will only get linkifed if the declared class is internal one, and not an extension :( 1335 $parent = ($type !== 'property') ? $reflector : $reflector->getDeclaringClass(); 1336 1337 // internal function/method/class/property/constant 1338 if ($parent->isInternal()) 1339 { 1340 $args[] = $reflector->name; 1341 1342 if (in_array($type, array('method', 'property'), true)) 1343 { 1344 $args[] = $reflector->getDeclaringClass()->getName(); 1345 } 1346 1347 $args = array_map( 1348 function ($text) 1349 { 1350 return str_replace('_', '-', ltrim(strtolower($text), '\\_')); 1351 }, $args); 1352 1353 // check for some special cases that have no links 1354 $valid = (($type === 'method') || (strcasecmp($parent->name, 'stdClass') !== 0)) 1355 && (($type !== 'method') || (($reflector->name === '__construct') || strpos($reflector->name, '__') !== 0)); 1356 1357 if ($valid) 1358 { 1359 $url = vsprintf($phpNetSchemes[$type], $args); 1360 } 1361 1362 // custom 1363 } else { 1364 switch (true) 1365 { 1366 1367 // WordPress function; 1368 // like pretty much everything else in WordPress, API links are inconsistent as well; 1369 // so we're using queryposts.com as doc source for API 1370 case ($type === 'function') && class_exists('WP', false) && defined('ABSPATH') && defined('WPINC'): 1371 if (strpos($reflector->getFileName(), realpath(ABSPATH . WPINC)) === 0) 1372 { 1373 $url = sprintf('http://queryposts.com/function/%s', urlencode(strtolower($reflector->getName()))); 1374 break; 1375 } 1376 1377 // @todo: handle more apps 1378 } 1379 1380 } 1381 1382 return $url; 1383 } 1384 1385 1386 public static function getTimeoutPoint() 1387 { 1388 return static::$timeout; 1389 } 1390 1391 1392 public static function getDebugInfo() 1393 { 1394 return static::$debug; 1395 } 1396 1397 1398 1399 protected function hasInstanceTimedOut() 1400 { 1401 1402 if (static::$timeout > 0) 1403 { 1404 return true; 1405 } 1406 1407 $timeout = static::$config['timeout']; 1408 1409 if (($timeout > 0) && ((microtime(true) - $this->startTime) > $timeout)) 1410 { 1411 return (static::$timeout = (microtime(true) - $this->startTime)); 1412 } 1413 1414 return false; 1415 } 1416 1417 1418 /** 1419 * Evaluates the given variable 1420 * 1421 * @param mixed &$subject Variable to query 1422 * @param bool $specialStr Should this be interpreted as a special string? 1423 * @return mixed Result (both HTML and text modes generate strings) 1424 */ 1425 protected function evaluate(&$subject, $specialStr = false) 1426 { 1427 1428 switch ($type = gettype($subject)) 1429 { 1430 1431 // https://github.com/digitalnature/php-ref/issues/13 1432 case 'unknown type': 1433 return $this->fmt->text('unknown'); 1434 1435 // null value 1436 case 'NULL': 1437 return $this->fmt->text('null'); 1438 1439 // integer/double/float 1440 case 'integer': 1441 case 'double': 1442 return $this->fmt->text($type, $subject, $type); 1443 1444 // boolean 1445 case 'boolean': 1446 $text = $subject ? 'true' : 'false'; 1447 return $this->fmt->text($text, $text, $type); 1448 1449 // arrays 1450 case 'array': 1451 1452 // empty array? 1453 if (empty($subject)) 1454 { 1455 $this->fmt->text('array'); 1456 return $this->fmt->emptyGroup(); 1457 } 1458 1459 if (isset($subject[static::MARKER_KEY])) 1460 { 1461 unset($subject[static::MARKER_KEY]); 1462 $this->fmt->text('array'); 1463 $this->fmt->emptyGroup('recursion'); 1464 return; 1465 } 1466 1467 // first recursion level detection; 1468 // this is optional (used to print consistent recursion info) 1469 foreach ($subject as $key => &$value) 1470 { 1471 1472 if (!is_array($value)) 1473 { 1474 continue; 1475 } 1476 1477 // save current value in a temporary variable 1478 $buffer = $value; 1479 1480 // assign new value 1481 $value = ($value !== 1) ? 1 : 2; 1482 1483 // if they're still equal, then we have a reference 1484 if ($value === $subject) 1485 { 1486 $value = $buffer; 1487 $value[static::MARKER_KEY] = true; 1488 $this->evaluate($value); 1489 return; 1490 } 1491 1492 // restoring original value 1493 $value = $buffer; 1494 } 1495 1496 $this->fmt->text('array'); 1497 $count = count($subject); 1498 if (!$this->fmt->startGroup($count)) 1499 { 1500 return; 1501 } 1502 1503 $max = max(array_map('static::strLen', array_keys($subject))); 1504 $subject[static::MARKER_KEY] = true; 1505 1506 foreach ($subject as $key => &$value) 1507 { 1508 1509 // ignore our temporary marker 1510 if ($key === static::MARKER_KEY) 1511 { 1512 continue; 1513 } 1514 1515 if ($this->hasInstanceTimedOut()) 1516 { 1517 break; 1518 } 1519 1520 $keyInfo = gettype($key); 1521 1522 if ($keyInfo === 'string') 1523 { 1524 $encoding = static::$env['mbStr'] ? mb_detect_encoding($key) : ''; 1525 $keyLen = $encoding && ($encoding !== 'ASCII') ? static::strLen($key) . '; ' . $encoding : static::strLen($key); 1526 $keyInfo = "{$keyInfo}({$keyLen})"; 1527 } else { 1528 $keyLen = strlen($key); 1529 } 1530 1531 $this->fmt->startRow(); 1532 $this->fmt->text('key', $key, "Key: {$keyInfo}"); 1533 $this->fmt->colDiv($max - $keyLen); 1534 $this->fmt->sep('=>'); 1535 $this->fmt->colDiv(); 1536 $this->evaluate($value, $specialStr); 1537 $this->fmt->endRow(); 1538 } 1539 1540 unset($subject[static::MARKER_KEY]); 1541 1542 $this->fmt->endGroup(); 1543 return; 1544 1545 // resource 1546 case 'resource': 1547 $meta = array(); 1548 $resType = get_resource_type($subject); 1549 1550 $this->fmt->text('resource', strval($subject)); 1551 1552 if (!static::$config['showResourceInfo']) 1553 { 1554 return $this->fmt->emptyGroup($resType); 1555 } 1556 1557 // @see: http://php.net/manual/en/resource.php 1558 // need to add more... 1559 switch ($resType) 1560 { 1561 1562 // curl extension resource 1563 case 'curl': 1564 $meta = curl_getinfo($subject); 1565 break; 1566 1567 case 'FTP Buffer': 1568 $meta = array( 1569 'time_out' => ftp_get_option($subject, FTP_TIMEOUT_SEC), 1570 'auto_seek' => ftp_get_option($subject, FTP_AUTOSEEK), 1571 ); 1572 1573 break; 1574 1575 // gd image extension resource 1576 case 'gd': 1577 $meta = array( 1578 'size' => sprintf('%d x %d', imagesx($subject), imagesy($subject)), 1579 'true_color' => imageistruecolor($subject), 1580 ); 1581 1582 break; 1583 1584 case 'ldap link': 1585 $constants = get_defined_constants(); 1586 1587 array_walk($constants, function($value, $key) use(&$constants) 1588 { 1589 if (strpos($key, 'LDAP_OPT_') !== 0) 1590 { 1591 unset($constants[$key]); 1592 } 1593 }); 1594 1595 // this seems to fail on my setup :( 1596 unset($constants['LDAP_OPT_NETWORK_TIMEOUT']); 1597 1598 foreach (array_slice($constants, 3) as $key => $value) 1599 { 1600 if (ldap_get_option($subject, (int)$value, $ret)) 1601 { 1602 $meta[strtolower(substr($key, 9))] = $ret; 1603 } 1604 } 1605 1606 break; 1607 1608 /* mysql connection (mysql extension is deprecated from php 5.4/5.5) 1609 case 'mysql link': 1610 case 'mysql link persistent': 1611 $dbs = array(); 1612 $query = @mysql_list_dbs($subject); 1613 while ($row = @mysql_fetch_array($query)) 1614 { 1615 $dbs[] = $row['Database']; 1616 } 1617 1618 $meta = array( 1619 'host' => ltrim(@mysql_get_host_info ($subject), 'MySQL host info: '), 1620 'server_version' => @mysql_get_server_info($subject), 1621 'protocol_version' => @mysql_get_proto_info($subject), 1622 'databases' => $dbs, 1623 ); 1624 1625 break; 1626 1627 // mysql result 1628 case 'mysql result': 1629 while ($row = @mysql_fetch_object($subject)) 1630 { 1631 $meta[] = (array)$row; 1632 1633 if ($this->hasInstanceTimedOut()) 1634 { 1635 break; 1636 } 1637 } 1638 1639 break; 1640 */ 1641 1642 // stream resource (fopen, fsockopen, popen, opendir etc) 1643 case 'stream': 1644 $meta = stream_get_meta_data($subject); 1645 break; 1646 1647 } 1648 1649 if (!$meta) 1650 { 1651 return $this->fmt->emptyGroup($resType); 1652 } 1653 1654 1655 if (!$this->fmt->startGroup($resType)) 1656 { 1657 return; 1658 } 1659 1660 $max = max(array_map('static::strLen', array_keys($meta))); 1661 foreach ($meta as $key => $value) 1662 { 1663 $this->fmt->startRow(); 1664 $this->fmt->text('resourceProp', ucwords(str_replace('_', ' ', $key))); 1665 $this->fmt->colDiv($max - static::strLen($key)); 1666 $this->fmt->sep(':'); 1667 $this->fmt->colDiv(); 1668 $this->evaluate($value); 1669 $this->fmt->endRow(); 1670 } 1671 $this->fmt->endGroup(); 1672 return; 1673 1674 // string 1675 case 'string': 1676 $length = static::strLen($subject); 1677 $encoding = static::$env['mbStr'] ? mb_detect_encoding($subject) : false; 1678 $info = $encoding && ($encoding !== 'ASCII') ? $length . '; ' . $encoding : $length; 1679 1680 if ($specialStr) 1681 { 1682 $this->fmt->sep('"'); 1683 $this->fmt->text(array('string', 'special'), $subject, "string({$info})"); 1684 $this->fmt->sep('"'); 1685 return; 1686 } 1687 1688 $this->fmt->text('string', $subject, "string({$info})"); 1689 1690 // advanced checks only if there are 3 characteres or more 1691 if (static::$config['showStringMatches'] && ($length > 2) && (trim($subject) !== '')) 1692 { 1693 1694 $isNumeric = is_numeric($subject); 1695 1696 // very simple check to determine if the string could match a file path 1697 // @note: this part of the code is very expensive 1698 $isFile = ($length < 2048) 1699 && (max(array_map('strlen', explode('/', str_replace('\\', '/', $subject)))) < 128) 1700 && !preg_match('/[^\w\.\-\/\\\\:]|\..*\.|\.$|:(?!(?<=^[a-zA-Z]:)[\/\\\\])/', $subject); 1701 1702 if ($isFile) 1703 { 1704 try 1705 { 1706 $file = new \SplFileInfo($subject); 1707 $flags = array(); 1708 $perms = $file->getPerms(); 1709 1710 if (($perms & 0xC000) === 0xC000) // socket 1711 { 1712 $flags[] = 's'; 1713 } 1714 else if (($perms & 0xA000) === 0xA000) // symlink 1715 { 1716 $flags[] = 'l'; 1717 } 1718 else if (($perms & 0x8000) === 0x8000) // regular 1719 { 1720 $flags[] = '-'; 1721 } 1722 else if (($perms & 0x6000) === 0x6000) // block special 1723 { 1724 $flags[] = 'b'; 1725 } 1726 else if (($perms & 0x4000) === 0x4000) // directory 1727 { 1728 $flags[] = 'd'; 1729 } 1730 else if (($perms & 0x2000) === 0x2000) // character special 1731 { 1732 $flags[] = 'c'; 1733 } 1734 else if (($perms & 0x1000) === 0x1000) // FIFO pipe 1735 { 1736 $flags[] = 'p'; 1737 } 1738 else // unknown 1739 { 1740 $flags[] = 'u'; 1741 } 1742 1743 // owner 1744 $flags[] = (($perms & 0x0100) ? 'r' : '-'); 1745 $flags[] = (($perms & 0x0080) ? 'w' : '-'); 1746 $flags[] = (($perms & 0x0040) ? (($perms & 0x0800) ? 's' : 'x' ) : (($perms & 0x0800) ? 'S' : '-')); 1747 1748 // group 1749 $flags[] = (($perms & 0x0020) ? 'r' : '-'); 1750 $flags[] = (($perms & 0x0010) ? 'w' : '-'); 1751 $flags[] = (($perms & 0x0008) ? (($perms & 0x0400) ? 's' : 'x' ) : (($perms & 0x0400) ? 'S' : '-')); 1752 1753 // world 1754 $flags[] = (($perms & 0x0004) ? 'r' : '-'); 1755 $flags[] = (($perms & 0x0002) ? 'w' : '-'); 1756 $flags[] = (($perms & 0x0001) ? (($perms & 0x0200) ? 't' : 'x' ) : (($perms & 0x0200) ? 'T' : '-')); 1757 1758 $size = is_dir($subject) ? '' : sprintf(' %.2fK', $file->getSize() / 1024); 1759 1760 $this->fmt->startContain('file', true); 1761 $this->fmt->text('file', implode('', $flags) . $size); 1762 $this->fmt->endContain(); 1763 1764 } catch(\Exception $e) { 1765 $isFile = false; 1766 } 1767 } 1768 1769 // class/interface/function 1770 if (!preg_match('/[^\w+\\\\]/', $subject) && ($length < 96)) 1771 { 1772 $isClass = class_exists($subject, false); 1773 if ($isClass) 1774 { 1775 $this->fmt->startContain('class', true); 1776 $this->fromReflector(new \ReflectionClass($subject)); 1777 $this->fmt->endContain(); 1778 } 1779 1780 if (!$isClass && interface_exists($subject, false)) 1781 { 1782 $this->fmt->startContain('interface', true); 1783 $this->fromReflector(new \ReflectionClass($subject)); 1784 $this->fmt->endContain('interface'); 1785 } 1786 1787 if (function_exists($subject)) 1788 { 1789 $this->fmt->startContain('function', true); 1790 $this->fromReflector(new \ReflectionFunction($subject)); 1791 $this->fmt->endContain('function'); 1792 } 1793 } 1794 1795 1796 // skip serialization/json/date checks if the string appears to be numeric, 1797 // or if it's shorter than 5 characters 1798 if (!$isNumeric && ($length > 4)) 1799 { 1800 1801 // url 1802 if (static::$config['showUrls'] && static::$env['curlActive'] && filter_var($subject, FILTER_VALIDATE_URL)) 1803 { 1804 $ch = curl_init($subject); 1805 curl_setopt($ch, CURLOPT_NOBODY, true); 1806 curl_exec($ch); 1807 $nfo = curl_getinfo($ch); 1808 curl_close($ch); 1809 1810 if ($nfo['http_code']) 1811 { 1812 $this->fmt->startContain('url', true); 1813 $contentType = explode(';', $nfo['content_type']); 1814 $this->fmt->text('url', sprintf('%s:%d %s %.2fms (%d)', !empty($nfo['primary_ip']) ? $nfo['primary_ip'] : null, !empty($nfo['primary_port']) ? $nfo['primary_port'] : null, $contentType[0], $nfo['total_time'], $nfo['http_code'])); 1815 $this->fmt->endContain(); 1816 } 1817 1818 } 1819 1820 // date 1821 if (($length < 128) && static::$env['supportsDate'] && !preg_match('/[^A-Za-z0-9.:+\s\-\/]/', $subject)) 1822 { 1823 try 1824 { 1825 $date = new \DateTime($subject); 1826 $errors = \DateTime::getLastErrors(); 1827 1828 if (($errors['warning_count'] < 1) && ($errors['error_count'] < 1)) 1829 { 1830 $now = new \Datetime('now'); 1831 $nowUtc = new \Datetime('now', new \DateTimeZone('UTC')); 1832 $diff = $now->diff($date); 1833 1834 $map = array( 1835 'y' => 'yr', 1836 'm' => 'mo', 1837 'd' => 'da', 1838 'h' => 'hr', 1839 'i' => 'min', 1840 's' => 'sec', 1841 ); 1842 1843 $timeAgo = 'now'; 1844 foreach ($map as $k => $label) 1845 { 1846 if ($diff->{$k} > 0) 1847 { 1848 $timeAgo = $diff->format("%R%{$k}{$label}"); 1849 break; 1850 } 1851 } 1852 1853 $tz = $date->getTimezone(); 1854 $offs = round($tz->getOffset($nowUtc) / 3600); 1855 1856 if ($offs > 0) 1857 { 1858 $offs = "+{$offs}"; 1859 } 1860 1861 $timeAgo .= ((int)$offs !== 0) ? ' ' . sprintf('%s (UTC%s)', $tz->getName(), $offs) : ' UTC'; 1862 $this->fmt->startContain('date', true); 1863 $this->fmt->text('date', $timeAgo); 1864 $this->fmt->endContain(); 1865 1866 } 1867 } catch(\Exception $e) { 1868 // not a date 1869 } 1870 1871 } 1872 1873 // attempt to detect if this is a serialized string 1874 static $unserializing = 0; 1875 $isSerialized = ($unserializing < 3) 1876 && (($subject[$length - 1] === ';') || ($subject[$length - 1] === '}')) 1877 && in_array($subject[0], array('s', 'a', 'O'), true) 1878 && ((($subject[0] === 's') && ($subject[$length - 2] !== '"')) || preg_match("/^{$subject[0]}:[0-9]+:/s", $subject)) 1879 && (($unserialized = @unserialize($subject)) !== false); 1880 1881 if ($isSerialized) 1882 { 1883 $unserializing++; 1884 $this->fmt->startContain('serialized', true); 1885 $this->evaluate($unserialized); 1886 $this->fmt->endContain(); 1887 $unserializing--; 1888 } 1889 1890 // try to find out if it's a json-encoded string; 1891 // only do this for json-encoded arrays or objects, because other types have too generic formats 1892 static $decodingJson = 0; 1893 $isJson = !$isSerialized && ($decodingJson < 3) && in_array($subject[0], array('{', '['), true); 1894 1895 if ($isJson) 1896 { 1897 $decodingJson++; 1898 $data = json_decode($subject); 1899 1900 // ensure created objects live enough for PHP to provide a unique hash 1901 if (is_object($data)) 1902 { 1903 $this->intObjects->attach($data); 1904 } 1905 1906 if ($isJson = (json_last_error() === JSON_ERROR_NONE)) 1907 { 1908 $this->fmt->startContain('json', true); 1909 $this->evaluate($data); 1910 $this->fmt->endContain(); 1911 } 1912 1913 $decodingJson--; 1914 } 1915 1916 // attempt to match a regex 1917 if (!$isSerialized && !$isJson && $length < 768) 1918 { 1919 try 1920 { 1921 $components = $this->splitRegex($subject); 1922 if ($components) 1923 { 1924 $regex = ''; 1925 1926 $this->fmt->startContain('regex', true); 1927 foreach ($components as $component) 1928 { 1929 $this->fmt->text('regex-' . key($component), reset($component)); 1930 } 1931 $this->fmt->endContain(); 1932 } 1933 1934 } catch(\Exception $e) { 1935 // not a regex 1936 } 1937 1938 } 1939 } 1940 } 1941 1942 return; 1943 } 1944 1945 // if we reached this point, $subject must be an object 1946 1947 // track objects to detect recursion 1948 static $hashes = array(); 1949 1950 // hash ID of this object 1951 $hash = spl_object_hash($subject); 1952 $recursion = isset($hashes[$hash]); 1953 1954 // sometimes incomplete objects may be created from string unserialization, 1955 // if the class to which the object belongs wasn't included until the unserialization stage... 1956 if ($subject instanceof \__PHP_Incomplete_Class) 1957 { 1958 $this->fmt->text('object'); 1959 $this->fmt->emptyGroup('incomplete'); 1960 return; 1961 } 1962 1963 // check cache at this point 1964 if (!$recursion && $this->fmt->didCache($hash)) 1965 { 1966 static::$debug['cacheHits']++; 1967 return; 1968 } 1969 1970 $reflector = new \ReflectionObject($subject); 1971 $this->fmt->startContain('class'); 1972 $this->fromReflector($reflector); 1973 $this->fmt->text('object', ' object'); 1974 $this->fmt->endContain(); 1975 1976 // already been here? 1977 if ($recursion) 1978 { 1979 return $this->fmt->emptyGroup('recursion'); 1980 } 1981 1982 $hashes[$hash] = 1; 1983 1984 $flags = \ReflectionProperty::IS_PUBLIC | \ReflectionProperty::IS_PROTECTED; 1985 1986 if (static::$config['showPrivateMembers']) 1987 { 1988 $flags |= \ReflectionProperty::IS_PRIVATE; 1989 } 1990 1991 $props = $reflector->getProperties($flags); 1992 $methods = array(); 1993 1994 if (static::$config['showMethods']) 1995 { 1996 $flags = \ReflectionMethod::IS_PUBLIC | \ReflectionMethod::IS_PROTECTED; 1997 1998 if (static::$config['showPrivateMembers']) 1999 { 2000 $flags |= \ReflectionMethod::IS_PRIVATE; 2001 } 2002 2003 $methods = $reflector->getMethods($flags); 2004 } 2005 2006 $constants = $reflector->getConstants(); 2007 $interfaces = $reflector->getInterfaces(); 2008 $traits = static::$env['is54'] ? $reflector->getTraits() : array(); 2009 $parents = static::getParentClasses($reflector); 2010 2011 // work-around for https://bugs.php.net/bug.php?id=49154 2012 // @see http://stackoverflow.com/questions/15672287/strange-behavior-of-reflectiongetproperties-with-numeric-keys 2013 if (!static::$env['is54']) 2014 { 2015 $props = array_values(array_filter($props, function($prop) use($subject) 2016 { 2017 return !$prop->isPublic() || property_exists($subject, $prop->name); 2018 })); 2019 } 2020 2021 // no data to display? 2022 if (!$props && !$methods && !$constants && !$interfaces && !$traits) 2023 { 2024 unset($hashes[$hash]); 2025 return $this->fmt->emptyGroup(); 2026 } 2027 2028 if (!$this->fmt->startGroup()) 2029 { 2030 return; 2031 } 2032 2033 // show contents for iterators 2034 if (static::$config['showIteratorContents'] && $reflector->isIterateable()) 2035 { 2036 2037 $itContents = iterator_to_array($subject); 2038 $this->fmt->sectionTitle(sprintf('Contents (%d)', count($itContents))); 2039 2040 foreach ($itContents as $key => $value) 2041 { 2042 $keyInfo = gettype($key); 2043 if ($keyInfo === 'string') 2044 { 2045 $encoding = static::$env['mbStr'] ? mb_detect_encoding($key) : ''; 2046 $length = $encoding && ($encoding !== 'ASCII') ? static::strLen($key) . '; ' . $encoding : static::strLen($key); 2047 $keyInfo = sprintf('%s(%s)', $keyInfo, $length); 2048 } 2049 2050 $this->fmt->startRow(); 2051 $this->fmt->text(array('key', 'iterator'), $key, sprintf('Iterator key: %s', $keyInfo)); 2052 $this->fmt->colDiv(); 2053 $this->fmt->sep('=>'); 2054 $this->fmt->colDiv(); 2055 $this->evaluate($value); 2056 //$this->evaluate($value instanceof \Traversable ? ((count($value) > 0) ? $value : (string)$value) : $value); 2057 $this->fmt->endRow(); 2058 } 2059 } 2060 2061 // display the interfaces this objects' class implements 2062 if ($interfaces) 2063 { 2064 $items = array(); 2065 $this->fmt->sectionTitle('Implements'); 2066 $this->fmt->startRow(); 2067 $this->fmt->startContain('interfaces'); 2068 2069 $i = 0; 2070 $count = count($interfaces); 2071 2072 foreach ($interfaces as $name => $interface) 2073 { 2074 $this->fromReflector($interface); 2075 2076 if (++$i < $count) 2077 { 2078 $this->fmt->sep(', '); 2079 } 2080 } 2081 2082 $this->fmt->endContain(); 2083 $this->fmt->endRow(); 2084 } 2085 2086 // traits this objects' class uses 2087 if ($traits) 2088 { 2089 $items = array(); 2090 $this->fmt->sectionTitle('Uses'); 2091 $this->fmt->startRow(); 2092 $this->fmt->startContain('traits'); 2093 2094 $i = 0; 2095 $count = count($traits); 2096 2097 foreach ($traits as $name => $trait) 2098 { 2099 $this->fromReflector($trait); 2100 2101 if (++$i < $count) 2102 { 2103 $this->fmt->sep(', '); 2104 } 2105 } 2106 2107 $this->fmt->endContain(); 2108 $this->fmt->endRow(); 2109 } 2110 2111 // class constants 2112 if ($constants) 2113 { 2114 $this->fmt->sectionTitle('Constants'); 2115 $max = max(array_map('static::strLen', array_keys($constants))); 2116 foreach ($constants as $name => $value) 2117 { 2118 $meta = null; 2119 $type = array('const'); 2120 foreach ($parents as $parent) 2121 { 2122 if ($parent->hasConstant($name)) 2123 { 2124 if ($parent !== $reflector) 2125 { 2126 $type[] = 'inherited'; 2127 $meta = array('sub' => array(array('Prototype defined by', $parent->name))); 2128 } 2129 break; 2130 } 2131 } 2132 2133 $this->fmt->startRow(); 2134 $this->fmt->sep('::'); 2135 $this->fmt->colDiv(); 2136 $this->fmt->startContain($type); 2137 $this->fmt->text('name', $name, $meta, $this->linkify($parent, $name)); 2138 $this->fmt->endContain(); 2139 $this->fmt->colDiv($max - static::strLen($name)); 2140 $this->fmt->sep('='); 2141 $this->fmt->colDiv(); 2142 $this->evaluate($value); 2143 $this->fmt->endRow(); 2144 } 2145 } 2146 2147 // object/class properties 2148 if ($props) 2149 { 2150 $this->fmt->sectionTitle('Properties'); 2151 2152 $max = 0; 2153 foreach ($props as $idx => $prop) 2154 { 2155 if (($propNameLen = static::strLen($prop->name)) > $max) 2156 { 2157 $max = $propNameLen; 2158 } 2159 } 2160 2161 foreach($props as $idx => $prop) 2162 { 2163 2164 if ($this->hasInstanceTimedOut()) 2165 { 2166 break; 2167 } 2168 2169 $bubbles = array(); 2170 $sourceClass = $prop->getDeclaringClass(); 2171 $inherited = $reflector->getShortName() !== $sourceClass->getShortName(); 2172 $meta = $sourceClass->isInternal() ? null : static::parseComment($prop->getDocComment()); 2173 2174 if ($meta) 2175 { 2176 if ($inherited) 2177 { 2178 $meta['sub'] = array(array('Declared in', $sourceClass->getShortName())); 2179 } 2180 2181 if (isset($meta['tags']['var'][0])) 2182 { 2183 $meta['left'] = $meta['tags']['var'][0][0]; 2184 } 2185 2186 unset($meta['tags']); 2187 } 2188 2189 if ($prop->isProtected() || $prop->isPrivate()) 2190 { 2191 $prop->setAccessible(true); 2192 } 2193 2194 $value = $prop->getValue($subject); 2195 2196 $this->fmt->startRow(); 2197 $this->fmt->sep($prop->isStatic() ? '::' : '->'); 2198 $this->fmt->colDiv(); 2199 2200 $bubbles = array(); 2201 if ($prop->isProtected()) 2202 { 2203 $bubbles[] = array('P', 'Protected'); 2204 } 2205 2206 if ($prop->isPrivate()) 2207 { 2208 $bubbles[] = array('!', 'Private'); 2209 } 2210 2211 $this->fmt->bubbles($bubbles); 2212 2213 $type = array('prop'); 2214 2215 if ($inherited) 2216 { 2217 $type[] = 'inherited'; 2218 } 2219 2220 if ($prop->isPrivate()) 2221 { 2222 $type[] = 'private'; 2223 } 2224 2225 $this->fmt->colDiv(2 - count($bubbles)); 2226 $this->fmt->startContain($type); 2227 $this->fmt->text('name', $prop->name, $meta, $this->linkify($prop)); 2228 $this->fmt->endContain(); 2229 $this->fmt->colDiv($max - static::strLen($prop->name)); 2230 $this->fmt->sep('='); 2231 $this->fmt->colDiv(); 2232 $this->evaluate($value); 2233 $this->fmt->endRow(); 2234 } 2235 } 2236 2237 // class methods 2238 if ($methods && !$this->hasInstanceTimedOut()) 2239 { 2240 2241 $this->fmt->sectionTitle('Methods'); 2242 foreach ($methods as $idx => $method) 2243 { 2244 2245 $this->fmt->startRow(); 2246 $this->fmt->sep($method->isStatic() ? '::' : '->'); 2247 $this->fmt->colDiv(); 2248 2249 $bubbles = array(); 2250 if ($method->isAbstract()) 2251 { 2252 $bubbles[] = array('A', 'Abstract'); 2253 } 2254 2255 if ($method->isFinal()) 2256 { 2257 $bubbles[] = array('F', 'Final'); 2258 } 2259 2260 if ($method->isProtected()) 2261 { 2262 $bubbles[] = array('P', 'Protected'); 2263 } 2264 2265 if ($method->isPrivate()) 2266 { 2267 $bubbles[] = array('!', 'Private'); 2268 } 2269 2270 $this->fmt->bubbles($bubbles); 2271 2272 $this->fmt->colDiv(4 - count($bubbles)); 2273 2274 // is this method inherited? 2275 $inherited = $reflector->getShortName() !== $method->getDeclaringClass()->getShortName(); 2276 2277 $type = array('method'); 2278 2279 if ($inherited) 2280 { 2281 $type[] = 'inherited'; 2282 } 2283 2284 if ($method->isPrivate()) 2285 { 2286 $type[] = 'private'; 2287 } 2288 2289 $this->fmt->startContain($type); 2290 2291 $name = $method->name; 2292 if ($method->returnsReference()) 2293 { 2294 $name = "&{$name}"; 2295 } 2296 2297 $this->fromReflector($method, $name, $reflector); 2298 2299 $paramCom = $method->isInternal() ? array() : static::parseComment($method->getDocComment(), 'tags'); 2300 $paramCom = empty($paramCom['param']) ? array() : $paramCom['param']; 2301 $paramCount = $method->getNumberOfParameters(); 2302 2303 $this->fmt->sep('('); 2304 2305 // process arguments 2306 foreach ($method->getParameters() as $idx => $parameter) 2307 { 2308 $meta = null; 2309 $paramName = "\${$parameter->name}"; 2310 $optional = $parameter->isOptional(); 2311 $variadic = static::$env['is56'] && $parameter->isVariadic(); 2312 2313 if ($parameter->isPassedByReference()) 2314 { 2315 $paramName = "&{$paramName}"; 2316 } 2317 2318 if ($variadic) 2319 { 2320 $paramName = "...{$paramName}"; 2321 } 2322 2323 $type = array('param'); 2324 2325 if ($optional) 2326 { 2327 $type[] = 'optional'; 2328 } 2329 2330 $this->fmt->startContain($type); 2331 2332 // attempt to build meta 2333 foreach ($paramCom as $tag) 2334 { 2335 list($pcTypes, $pcName, $pcDescription) = $tag; 2336 if ($pcName !== $paramName) 2337 { 2338 continue; 2339 } 2340 2341 $meta = array('title' => $pcDescription); 2342 2343 if ($pcTypes) 2344 { 2345 $meta['left'] = $pcTypes; 2346 } 2347 2348 break; 2349 } 2350 2351 try 2352 { 2353 $paramClass = $parameter->getClass(); 2354 } catch(\Exception $e) { 2355 // @see https://bugs.php.net/bug.php?id=32177&edit=1 2356 } 2357 2358 if (!empty($paramClass)) 2359 { 2360 $this->fmt->startContain('hint'); 2361 $this->fromReflector($paramClass, $paramClass->name); 2362 $this->fmt->endContain(); 2363 $this->fmt->sep(' '); 2364 2365 } 2366 else if ($parameter->isArray()) 2367 { 2368 $this->fmt->text('hint', 'array'); 2369 $this->fmt->sep(' '); 2370 2371 } else { 2372 $hasType = static::$env['is7'] && $parameter->hasType(); 2373 if ($hasType) 2374 { 2375 $type = $parameter->getType(); 2376 $this->fmt->text('hint', (string)$type); 2377 $this->fmt->sep(' '); 2378 } 2379 } 2380 2381 $this->fmt->text('name', $paramName, $meta); 2382 2383 if ($optional) 2384 { 2385 $paramValue = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; 2386 $this->fmt->sep(' = '); 2387 2388 if (static::$env['is546'] && !$parameter->getDeclaringFunction()->isInternal() && $parameter->isDefaultValueConstant()) 2389 { 2390 $this->fmt->text('constant', $parameter->getDefaultValueConstantName(), 'Constant'); 2391 2392 } else { 2393 $this->evaluate($paramValue, true); 2394 } 2395 } 2396 2397 $this->fmt->endContain(); 2398 2399 if ($idx < $paramCount - 1) 2400 { 2401 $this->fmt->sep(', '); 2402 } 2403 } 2404 $this->fmt->sep(')'); 2405 $this->fmt->endContain(); 2406 2407 $hasReturnType = static::$env['is7'] && $method->hasReturnType(); 2408 if ($hasReturnType) 2409 { 2410 $type = $method->getReturnType(); 2411 $this->fmt->startContain('ret'); 2412 $this->fmt->sep(':'); 2413 $this->fmt->text('hint', (string)$type); 2414 $this->fmt->endContain(); 2415 } 2416 2417 $this->fmt->endRow(); 2418 } 2419 } 2420 2421 unset($hashes[$hash]); 2422 $this->fmt->endGroup(); 2423 2424 $this->fmt->cacheLock($hash); 2425 } 2426 2427 2428 /** 2429 * Scans for known classes and functions inside the provided expression, 2430 * and linkifies them when possible 2431 * 2432 * @param string $expression Expression to format 2433 * @return string Formatted output 2434 */ 2435 protected function evaluateExp($expression = null) 2436 { 2437 2438 if ($expression === null) 2439 { 2440 return; 2441 } 2442 2443 if (static::strLen($expression) > 120) 2444 { 2445 $expression = substr($expression, 0, 120) . '...'; 2446 } 2447 2448 $this->fmt->sep('> '); 2449 2450 if (strpos($expression, '(') === false) 2451 { 2452 return $this->fmt->text('expTxt', $expression); 2453 } 2454 2455 $keywords = array_map('trim', explode('(', $expression, 2)); 2456 $parts = array(); 2457 2458 // try to find out if this is a function 2459 try 2460 { 2461 $reflector = new \ReflectionFunction($keywords[0]); 2462 $parts[] = array($keywords[0], $reflector, ''); 2463 2464 } catch(\Exception $e) { 2465 2466 if (stripos($keywords[0], 'new ') === 0) 2467 { 2468 $cn = explode(' ' , $keywords[0], 2); 2469 2470 // linkify 'new keyword' (as constructor) 2471 try{ 2472 $reflector = new \ReflectionMethod($cn[1], '__construct'); 2473 $parts[] = array($cn[0], $reflector, ''); 2474 2475 } catch(\Exception $e) { 2476 $reflector = null; 2477 $parts[] = $cn[0]; 2478 } 2479 2480 // class name... 2481 try 2482 { 2483 $reflector = new \ReflectionClass($cn[1]); 2484 $parts[] = array($cn[1], $reflector, ' '); 2485 2486 } catch(\Exception $e) { 2487 $reflector = null; 2488 $parts[] = $cn[1]; 2489 } 2490 2491 } else { 2492 2493 // we can only linkify methods called statically 2494 if (strpos($keywords[0], '::') === false) 2495 { 2496 return $this->fmt->text('expTxt', $expression); 2497 } 2498 2499 $cn = explode('::', $keywords[0], 2); 2500 2501 // attempt to linkify class name 2502 try 2503 { 2504 $reflector = new \ReflectionClass($cn[0]); 2505 $parts[] = array($cn[0], $reflector, ''); 2506 2507 } catch(\Exception $e) { 2508 $reflector = null; 2509 $parts[] = $cn[0]; 2510 } 2511 2512 // perhaps it's a static class method; try to linkify method 2513 try 2514 { 2515 $reflector = new \ReflectionMethod($cn[0], $cn[1]); 2516 $parts[] = array($cn[1], $reflector, '::'); 2517 2518 } catch(\Exception $e) 2519 { 2520 $reflector = null; 2521 $parts[] = $cn[1]; 2522 } 2523 } 2524 } 2525 2526 $parts[] = "({$keywords[1]}"; 2527 2528 foreach ($parts as $element) 2529 { 2530 if (!is_array($element)) 2531 { 2532 $this->fmt->text('expTxt', $element); 2533 continue; 2534 } 2535 2536 list($text, $reflector, $prefix) = $element; 2537 2538 if ($prefix !== '') 2539 { 2540 $this->fmt->text('expTxt', $prefix); 2541 } 2542 2543 $this->fromReflector($reflector, $text); 2544 } 2545 2546 } 2547 2548 2549 /** 2550 * Calculates real string length 2551 * 2552 * @param string $string 2553 * @return int 2554 */ 2555 protected static function strLen($string) 2556 { 2557 $encoding = function_exists('mb_detect_encoding') ? mb_detect_encoding($string) : false; 2558 return $encoding ? mb_strlen($string, $encoding) : strlen($string); 2559 } 2560 2561 2562 /** 2563 * Safe str_pad alternative 2564 * 2565 * @param string $string 2566 * @param int $padLen 2567 * @param string $padStr 2568 * @param int $padType 2569 * @return string 2570 */ 2571 protected static function strPad($input, $padLen, $padStr = ' ', $padType = STR_PAD_RIGHT) 2572 { 2573 $diff = strlen($input) - static::strLen($input); 2574 return str_pad($input, $padLen + $diff, $padStr, $padType); 2575 } 2576 2577} 2578 2579 2580 2581/** 2582 * Formatter abstraction 2583 */ 2584abstract class RFormatter 2585{ 2586 2587 /** 2588 * Flush output and send contents to the output device 2589 */ 2590 abstract public function flush(); 2591 2592 /** 2593 * Generate a base entity 2594 * 2595 * @param string|array $type 2596 * @param string|null $text 2597 * @param string|array|null $meta 2598 * @param string|null $uri 2599 */ 2600 abstract public function text($type, $text = null, $meta = null, $uri = null); 2601 2602 /** 2603 * Generate container start token 2604 * 2605 * @param string|array $type 2606 * @param string|bool $label 2607 */ 2608 public function startContain($type, $label = false){} 2609 2610 /** 2611 * Generate container ending token 2612 */ 2613 public function endContain(){} 2614 2615 /** 2616 * Generate empty group token 2617 * 2618 * @param string $prefix 2619 */ 2620 public function emptyGroup($prefix = ''){} 2621 2622 /** 2623 * Generate group start token 2624 * 2625 * This method must return boolean TRUE on success, false otherwise (eg. max depth reached). 2626 * The evaluator will skip this group on FALSE 2627 * 2628 * @param string $prefix 2629 * @return bool 2630 */ 2631 public function startGroup($prefix = ''){} 2632 2633 /** 2634 * Generate group ending token 2635 */ 2636 public function endGroup(){} 2637 2638 /** 2639 * Generate section title 2640 * 2641 * @param string $title 2642 */ 2643 public function sectionTitle($title){} 2644 2645 /** 2646 * Generate row start token 2647 */ 2648 public function startRow(){} 2649 2650 /** 2651 * Generate row ending token 2652 */ 2653 public function endRow(){} 2654 2655 /** 2656 * Column divider (cell delimiter) 2657 * 2658 * @param int $padLen 2659 */ 2660 public function colDiv($padLen = null){} 2661 2662 /** 2663 * Generate modifier tokens 2664 * 2665 * @param array $items 2666 */ 2667 public function bubbles(array $items){} 2668 2669 /** 2670 * Input expression start 2671 */ 2672 public function startExp(){} 2673 2674 /** 2675 * Input expression end 2676 */ 2677 public function endExp(){} 2678 2679 /** 2680 * Root starting token 2681 */ 2682 public function startRoot(){} 2683 2684 /** 2685 * Root ending token 2686 */ 2687 public function endRoot(){} 2688 2689 /** 2690 * Separator token 2691 * 2692 * @param string $label 2693 */ 2694 public function sep($label = ' '){} 2695 2696 /** 2697 * Resolve cache request 2698 * 2699 * If the ID is not present in the cache, then a new cache entry is created 2700 * for the given ID, and string offsets are captured until cacheLock is called 2701 * 2702 * This method must return TRUE if the ID exists in the cache, and append the cached item 2703 * to the output, FALSE otherwise. 2704 * 2705 * @param string $id 2706 * @return bool 2707 */ 2708 public function didCache($id) 2709 { 2710 return false; 2711 } 2712 2713 /** 2714 * Ends cache capturing for the given ID 2715 * 2716 * @param string $id 2717 */ 2718 public function cacheLock($id){} 2719 2720} 2721 2722 2723/** 2724 * Generates the output in HTML5 format 2725 * 2726 */ 2727class RHtmlFormatter extends RFormatter{ 2728 2729 protected 2730 2731 /** 2732 * Actual output 2733 * 2734 * @var string 2735 */ 2736 $out = '', 2737 2738 /** 2739 * Tracks current nesting level 2740 * 2741 * @var int 2742 */ 2743 $level = 0, 2744 2745 /** 2746 * Stores tooltip content for all entries 2747 * 2748 * To avoid having duplicate tooltip data in the HTML, we generate them once, 2749 * and use references (the Q index) to pull data when required; 2750 * this improves performance significantly 2751 * 2752 * @var array 2753 */ 2754 $tips = array(), 2755 2756 /** 2757 * Used to cache output to speed up processing. 2758 * 2759 * Contains hashes as keys and string offsets as values. 2760 * Cached objects will not be processed again in the same query 2761 * 2762 * @var array 2763 */ 2764 $cache = array(), 2765 2766 /** 2767 * Map of used HTML tag and attributes 2768 * 2769 * @var string 2770 */ 2771 $def = array(); 2772 2773 2774 2775 protected static 2776 2777 /** 2778 * Instance counter 2779 * 2780 * @var int 2781 */ 2782 $counter = 0, 2783 2784 /** 2785 * Tracks style/jscript inclusion state 2786 * 2787 * @var bool 2788 */ 2789 $didAssets = false; 2790 2791 2792 public function __construct() 2793 { 2794 2795 if (ref::config('validHtml')) 2796 { 2797 2798 $this->def = array( 2799 'base' => 'span', 2800 'tip' => 'div', 2801 'cell' => 'data-cell', 2802 'table' => 'data-table', 2803 'row' => 'data-row', 2804 'group' => 'data-group', 2805 'gLabel' => 'data-gLabel', 2806 'match' => 'data-match', 2807 'tipRef' => 'data-tip', 2808 ); 2809 2810 2811 } else { 2812 2813 $this->def = array( 2814 'base' => 'r', 2815 'tip' => 't', 2816 'cell' => 'c', 2817 'table' => 't', 2818 'row' => 'r', 2819 'group' => 'g', 2820 'gLabel' => 'gl', 2821 'match' => 'm', 2822 'tipRef' => 'h', 2823 ); 2824 2825 } 2826 2827 } 2828 2829 2830 2831 public function flush() 2832 { 2833 print $this->out; 2834 $this->out = ''; 2835 $this->cache = array(); 2836 $this->tips = array(); 2837 } 2838 2839 2840 public function didCache($id) 2841 { 2842 2843 if (!isset($this->cache[$id])) 2844 { 2845 $this->cache[$id] = array(); 2846 $this->cache[$id][] = strlen($this->out); 2847 return false; 2848 } 2849 2850 if (!isset($this->cache[$id][1])) 2851 { 2852 $this->cache[$id][0] = strlen($this->out); 2853 return false; 2854 } 2855 2856 $this->out .= substr($this->out, $this->cache[$id][0], $this->cache[$id][1]); 2857 return true; 2858 } 2859 2860 public function cacheLock($id) 2861 { 2862 $this->cache[$id][] = strlen($this->out) - $this->cache[$id][0]; 2863 } 2864 2865 public function sep($label = ' ') 2866 { 2867 $this->out .= $label !== ' ' ? '<i>' . static::escape($label) . '</i>' : $label; 2868 } 2869 2870 public function text($type, $text = null, $meta = null, $uri = null) 2871 { 2872 2873 if (!is_array($type)) 2874 { 2875 $type = (array)$type; 2876 } 2877 2878 $tip = ''; 2879 $text = ($text !== null) ? static::escape($text) : static::escape($type[0]); 2880 2881 if (in_array('special', $type)) 2882 { 2883 $text = strtr($text, array( 2884 "\r" => '<i>\r</i>', // carriage return 2885 "\t" => '<i>\t</i>', // horizontal tab 2886 "\n" => '<i>\n</i>', // linefeed (new line) 2887 "\v" => '<i>\v</i>', // vertical tab 2888 "\e" => '<i>\e</i>', // escape 2889 "\f" => '<i>\f</i>', // form feed 2890 "\0" => '<i>\0</i>', 2891 )); 2892 } 2893 2894 // generate tooltip reference (probably the slowest part of the code ;) 2895 if ($meta !== null) 2896 { 2897 $tipIdx = array_search($meta, $this->tips, true); 2898 2899 if ($tipIdx === false) 2900 { 2901 $tipIdx = array_push($this->tips, $meta) - 1; 2902 } 2903 2904 $tip = " {$this->def['tipRef']}=\"{$tipIdx}\""; 2905 //$tip = sprintf('%s="%d"', $this->def['tipRef'], $tipIdx); 2906 } 2907 2908 // wrap text in a link? 2909 if ($uri !== null) 2910 { 2911 $text = '<a href="' . $uri . '" target="_blank">' . $text . '</a>'; 2912 } 2913 2914 $typeStr = ''; 2915 foreach ($type as $part) 2916 { 2917 $typeStr .= " data-{$part}"; 2918 } 2919 2920 $this->out .= "<{$this->def['base']}{$typeStr}{$tip}>{$text}</{$this->def['base']}>"; 2921 //$this->out .= sprintf('<%1$s%2$s %3$s>%4$s</%1$s>', $this->def['base'], $typeStr, $tip, $text); 2922 } 2923 2924 public function startContain($type, $label = false) 2925 { 2926 2927 if (!is_array($type)) 2928 { 2929 $type = (array)$type; 2930 } 2931 2932 if ($label) 2933 { 2934 $this->out .= '<br>'; 2935 } 2936 2937 $typeStr = ''; 2938 foreach ($type as $part) 2939 { 2940 $typeStr .= " data-{$part}"; 2941 } 2942 2943 $this->out .= "<{$this->def['base']}{$typeStr}>"; 2944 2945 if ($label) 2946 { 2947 $this->out .= "<{$this->def['base']} {$this->def['match']}>{$type[0]}</{$this->def['base']}>"; 2948 } 2949 } 2950 2951 public function endContain() 2952 { 2953 $this->out .= "</{$this->def['base']}>"; 2954 } 2955 2956 public function emptyGroup($prefix = '') 2957 { 2958 2959 if ($prefix !== '') 2960 { 2961 $prefix = "<{$this->def['base']} {$this->def['gLabel']}>" . static::escape($prefix) . "</{$this->def['base']}>"; 2962 } 2963 2964 $this->out .= "<i>(</i>{$prefix}<i>)</i>"; 2965 } 2966 2967 2968 public function startGroup($prefix = '') 2969 { 2970 2971 $maxDepth = ref::config('maxDepth'); 2972 2973 if (($maxDepth > 0) && (($this->level + 1) > $maxDepth)) 2974 { 2975 $this->emptyGroup('...'); 2976 return false; 2977 } 2978 2979 $this->level++; 2980 2981 $expLvl = ref::config('expLvl'); 2982 $exp = ($expLvl < 0) || (($expLvl > 0) && ($this->level <= $expLvl)) ? ' data-exp' : ''; 2983 2984 if ($prefix !== '') 2985 { 2986 $prefix = "<{$this->def['base']} {$this->def['gLabel']}>" . static::escape($prefix) . "</{$this->def['base']}>"; 2987 } 2988 2989 $this->out .= "<i>(</i>{$prefix}<{$this->def['base']} data-toggle{$exp}></{$this->def['base']}><{$this->def['base']} {$this->def['group']}><{$this->def['base']} {$this->def['table']}>"; 2990 2991 return true; 2992 } 2993 2994 public function endGroup() 2995 { 2996 $this->out .= "</{$this->def['base']}></{$this->def['base']}><i>)</i>"; 2997 $this->level--; 2998 } 2999 3000 public function sectionTitle($title) 3001 { 3002 $this->out .= "</{$this->def['base']}><{$this->def['base']} data-tHead>{$title}</{$this->def['base']}><{$this->def['base']} {$this->def['table']}>"; 3003 } 3004 3005 public function startRow() 3006 { 3007 $this->out .= "<{$this->def['base']} {$this->def['row']}><{$this->def['base']} {$this->def['cell']}>"; 3008 } 3009 3010 public function endRow() 3011 { 3012 $this->out .= "</{$this->def['base']}></{$this->def['base']}>"; 3013 } 3014 3015 public function colDiv($padLen = null) 3016 { 3017 $this->out .= "</{$this->def['base']}><{$this->def['base']} {$this->def['cell']}>"; 3018 } 3019 3020 public function bubbles(array $items) 3021 { 3022 3023 if (!$items) 3024 { 3025 return; 3026 } 3027 3028 $this->out .= "<{$this->def['base']} data-mod>"; 3029 3030 foreach ($items as $info) 3031 { 3032 $this->out .= $this->text('mod-' . strtolower($info[1]), $info[0], $info[1]); 3033 } 3034 3035 $this->out .= "</{$this->def['base']}>"; 3036 } 3037 3038 public function startExp() 3039 { 3040 $this->out .= "<{$this->def['base']} data-input>"; 3041 } 3042 3043 public function endExp() 3044 { 3045 if (ref::config('showBacktrace') && ($trace = ref::getBacktrace())) 3046 { 3047 $docRoot = isset($_SERVER['DOCUMENT_ROOT']) ? $_SERVER['DOCUMENT_ROOT'] : ''; 3048 $path = strpos($trace['file'], $docRoot) !== 0 ? $trace['file'] : ltrim(str_replace($docRoot, '', $trace['file']), '/'); 3049 $this->out .= "<{$this->def['base']} data-backtrace>{$path}:{$trace['line']}</{$this->def['base']}>"; 3050 } 3051 3052 $this->out .= "</{$this->def['base']}><{$this->def['base']} data-output>"; 3053 } 3054 3055 public function startRoot() 3056 { 3057 $this->out .= '<!-- ref#' . ++static::$counter . ' --><div>' . static::getAssets() . '<div class="ref">'; 3058 } 3059 3060 public function endRoot() 3061 { 3062 $this->out .= "</{$this->def['base']}>"; 3063 3064 // process tooltips 3065 $tipHtml = ''; 3066 foreach ($this->tips as $idx => $meta) 3067 { 3068 3069 $tip = ''; 3070 if (!is_array($meta)) 3071 { 3072 $meta = array('title' => $meta); 3073 } 3074 3075 $meta += array( 3076 'title' => '', 3077 'left' => '', 3078 'description' => '', 3079 'tags' => array(), 3080 'sub' => array(), 3081 ); 3082 3083 $meta = static::escape($meta); 3084 $cols = array(); 3085 3086 if ($meta['left']) 3087 { 3088 $cols[] = "<{$this->def['base']} {$this->def['cell']} data-varType>{$meta['left']}</{$this->def['base']}>"; 3089 } 3090 3091 $title = $meta['title'] ? "<{$this->def['base']} data-title>{$meta['title']}</{$this->def['base']}>" : ''; 3092 $desc = $meta['description'] ? "<{$this->def['base']} data-desc>{$meta['description']}</{$this->def['base']}>" : ''; 3093 $tags = ''; 3094 3095 foreach ($meta['tags'] as $tag => $values) 3096 { 3097 foreach ($values as $value) 3098 { 3099 if ($tag === 'param') 3100 { 3101 $value[0] = "{$value[0]} {$value[1]}"; 3102 unset($value[1]); 3103 } 3104 3105 $value = is_array($value) ? implode("</{$this->def['base']}><{$this->def['base']} {$this->def['cell']}>", $value) : $value; 3106 $tags .= "<{$this->def['base']} {$this->def['row']}><{$this->def['base']} {$this->def['cell']}>@{$tag}</{$this->def['base']}><{$this->def['base']} {$this->def['cell']}>{$value}</{$this->def['base']}></{$this->def['base']}>"; 3107 } 3108 } 3109 3110 if ($tags) 3111 { 3112 $tags = "<{$this->def['base']} {$this->def['table']}>{$tags}</{$this->def['base']}>"; 3113 } 3114 3115 if ($title || $desc || $tags) 3116 { 3117 $cols[] = "<{$this->def['base']} {$this->def['cell']}>{$title}{$desc}{$tags}</{$this->def['base']}>"; 3118 } 3119 3120 if ($cols) 3121 { 3122 $tip = "<{$this->def['base']} {$this->def['row']}>" . implode('', $cols) . "</{$this->def['base']}>"; 3123 } 3124 3125 $sub = ''; 3126 foreach ($meta['sub'] as $line) 3127 { 3128 $sub .= "<{$this->def['base']} {$this->def['row']}><{$this->def['base']} {$this->def['cell']}>" . implode("</{$this->def['base']}><{$this->def['base']} {$this->def['cell']}>", $line) . "</{$this->def['base']}></{$this->def['base']}>"; 3129 } 3130 3131 if ($sub) 3132 { 3133 $tip .= "<{$this->def['base']} {$this->def['row']}><{$this->def['base']} {$this->def['cell']} data-sub><{$this->def['base']} {$this->def['table']}>{$sub}</{$this->def['base']}></{$this->def['base']}></{$this->def['base']}>"; 3134 } 3135 3136 if ($tip) 3137 { 3138 $this->out .= "<{$this->def['tip']}>{$tip}</{$this->def['tip']}>"; 3139 } 3140 } 3141 3142 if (($timeout = ref::getTimeoutPoint()) > 0) 3143 { 3144 $this->out .= sprintf("<{$this->def['base']} data-error>Listing incomplete. Timed-out after %4.2fs</{$this->def['base']}>", $timeout); 3145 } 3146 3147 $this->out .= '</div></div><!-- /ref#' . static::$counter . ' -->'; 3148 } 3149 3150 3151 /** 3152 * Get styles and javascript (only generated for the 1st call) 3153 * 3154 * @return string 3155 */ 3156 public static function getAssets() 3157 { 3158 3159 // first call? include styles and javascript 3160 if (static::$didAssets) 3161 { 3162 return ''; 3163 } 3164 3165 ob_start(); 3166 3167 if (ref::config('stylePath') !== false) 3168 { 3169 ?> 3170 <style> 3171 <?php readfile(str_replace('{:dir}', __DIR__, ref::config('stylePath'))); ?> 3172 </style> 3173 <?php 3174 } 3175 3176 if (ref::config('scriptPath') !== false) 3177 { 3178 ?> 3179 <script> 3180 <?php readfile(str_replace('{:dir}', __DIR__, ref::config('scriptPath'))); ?> 3181 </script> 3182 <?php 3183 } 3184 3185 // normalize space and remove comments 3186 $output = preg_replace('/\s+/', ' ', trim(ob_get_clean())); 3187 $output = preg_replace('!/\*.*?\*/!s', '', $output); 3188 $output = preg_replace('/\n\s*\n/', "\n", $output); 3189 3190 static::$didAssets = true; 3191 return $output; 3192 } 3193 3194 3195 /** 3196 * Escapes variable for HTML output 3197 * 3198 * @param string|array $var 3199 * @return string|array 3200 */ 3201 protected static function escape($var) 3202 { 3203 return is_array($var) ? array_map('static::escape', $var) : htmlspecialchars($var, ENT_QUOTES); 3204 } 3205 3206} 3207 3208 3209 3210/** 3211 * Generates the output in plain text format 3212 * 3213 */ 3214class RTextFormatter extends RFormatter 3215{ 3216 3217 protected 3218 3219 /** 3220 * Actual output 3221 * 3222 * @var string 3223 */ 3224 $out = '', 3225 3226 /** 3227 * Tracks current nesting level 3228 * 3229 * @var int 3230 */ 3231 $level = 0, 3232 3233 /** 3234 * Current indenting level 3235 * 3236 * @var int 3237 */ 3238 $indent = 0, 3239 3240 $lastIdx = 0, 3241 $lastLineSt = 0, 3242 $levelPad = array(0); 3243 3244 3245 public function flush() 3246 { 3247 print $this->out; 3248 $this->out = ''; 3249 $this->cache = array(); 3250 } 3251 3252 public function sep($label = ' ') 3253 { 3254 $this->out .= $label; 3255 } 3256 3257 public function text($type, $text = null, $meta = null, $uri = null) 3258 { 3259 3260 if (!is_array($type)) 3261 { 3262 $type = (array)$type; 3263 } 3264 3265 if ($text === null) 3266 { 3267 $text = $type[0]; 3268 } 3269 3270 if (in_array('special', $type, true)) 3271 { 3272 $text = strtr($text, array( 3273 "\r" => '\r', // carriage return 3274 "\t" => '\t', // horizontal tab 3275 "\n" => '\n', // linefeed (new line) 3276 "\v" => '\v', // vertical tab 3277 "\e" => '\e', // escape 3278 "\f" => '\f', // form feed 3279 "\0" => '\0', 3280 )); 3281 3282 $this->out .= $text; 3283 return; 3284 } 3285 3286 $formatMap = array( 3287 'string' => '%3$s "%2$s"', 3288 'integer' => 'int(%2$s)', 3289 'double' => 'double(%2$s)', 3290 'true' => 'bool(%2$s)', 3291 'false' => 'bool(%2$s)', 3292 'key' => '[%2$s]', 3293 ); 3294 3295 if (!is_string($meta)) 3296 { 3297 $meta = ''; 3298 } 3299 3300 $this->out .= isset($formatMap[$type[0]]) ? sprintf($formatMap[$type[0]], $type[0], $text, $meta) : $text; 3301 } 3302 3303 public function startContain($type, $label = false) 3304 { 3305 3306 if (!is_array($type)) 3307 { 3308 $type = (array)$type; 3309 } 3310 3311 if ($label) 3312 { 3313 $this->out .= "\n" . str_repeat(' ', $this->indent + $this->levelPad[$this->level]) . "┗ {$type[0]} ~ "; 3314 } 3315 } 3316 3317 public function emptyGroup($prefix = '') 3318 { 3319 $this->out .= "({$prefix})"; 3320 } 3321 3322 public function startGroup($prefix = '') 3323 { 3324 3325 $maxDepth = ref::config('maxDepth'); 3326 3327 if (($maxDepth > 0) && (($this->level + 1) > $maxDepth)) 3328 { 3329 $this->emptyGroup('...'); 3330 return false; 3331 } 3332 3333 $this->level++; 3334 $this->out .= '('; 3335 3336 $this->indent += $this->levelPad[$this->level - 1]; 3337 return true; 3338 } 3339 3340 public function endGroup() 3341 { 3342 $this->out .= "\n" . str_repeat(' ', $this->indent) . ')'; 3343 $this->indent -= $this->levelPad[$this->level - 1]; 3344 $this->level--; 3345 } 3346 3347 public function sectionTitle($title) 3348 { 3349 $pad = str_repeat(' ', $this->indent + 2); 3350 $this->out .= sprintf("\n\n%s%s\n%s%s", $pad, $title, $pad, str_repeat('-', strlen($title))); 3351 } 3352 3353 public function startRow() 3354 { 3355 $this->out .= "\n " . str_repeat(' ', $this->indent); 3356 $this->lastLineSt = strlen($this->out); 3357 } 3358 3359 public function endRow(){} 3360 3361 public function colDiv($padLen = null) 3362 { 3363 $padLen = ($padLen !== null) ? $padLen + 1 : 1; 3364 $this->out .= str_repeat(' ', $padLen); 3365 3366 $this->lastIdx = strlen($this->out); 3367 $this->levelPad[$this->level] = $this->lastIdx - $this->lastLineSt + 2; 3368 } 3369 3370 public function bubbles(array $items) 3371 { 3372 3373 if (!$items) 3374 { 3375 $this->out .= ' '; 3376 return; 3377 } 3378 3379 $this->out .= '<'; 3380 3381 foreach ($items as $item) 3382 { 3383 $this->out .= $item[0]; 3384 } 3385 3386 $this->out .= '>'; 3387 } 3388 3389 public function endExp() 3390 { 3391 3392 if (ref::config('showBacktrace') && ($trace = ref::getBacktrace())) 3393 { 3394 $this->out .= ' - ' . $trace['file'] . ':' . $trace['line']; 3395 } 3396 3397 $this->out .= "\n" . str_repeat('=', strlen($this->out)) . "\n"; 3398 } 3399 3400 public function startRoot() 3401 { 3402 $this->out .= "\n\n"; 3403 3404 } 3405 3406 public function endRoot() 3407 { 3408 $this->out .= "\n"; 3409 if (($timeout = ref::getTimeoutPoint()) > 0) 3410 { 3411 $this->out .= sprintf("\n-- Listing incomplete. Timed-out after %4.2fs -- \n", $timeout); 3412 } 3413 } 3414 3415} 3416 3417 3418/** 3419 * Text formatter with color support for CLI -- unfinished 3420 * 3421 */ 3422class RCliTextFormatter extends RTextFormatter 3423{ 3424 3425 public function sectionTitle($title) 3426 { 3427 $pad = str_repeat(' ', $this->indent + 2); 3428 $this->out .= sprintf("\n\n%s\x1b[4;97m%s\x1b[0m", $pad, $title); 3429 } 3430 3431 public function startExp() 3432 { 3433 $this->out .= "\x1b[1;44;96m "; 3434 } 3435 3436 public function endExp() 3437 { 3438 if (ref::config('showBacktrace') && ($trace = ref::getBacktrace())) 3439 { 3440 $this->out .= "\x1b[0m\x1b[44;36m " . $trace['file'] . ':' . $trace['line']; 3441 } 3442 3443 $this->out .= " \x1b[0m\n"; 3444 } 3445 3446 public function endRoot() 3447 { 3448 $this->out .= "\n"; 3449 if (($timeout = ref::getTimeoutPoint()) > 0) 3450 { 3451 $this->out .= sprintf("\n\x1b[3;91m-- Listing incomplete. Timed-out after %4.2fs --\x1b[0m\n", $timeout); 3452 } 3453 } 3454 3455} 3456 3457// EOF