From ddb4aebf60b21060f2b5afdefea66ce05e3c6ca5 Mon Sep 17 00:00:00 2001 From: Ryan Cramer Date: Fri, 26 Jul 2019 11:41:05 -0400 Subject: [PATCH] Add new static Debug::backtrace() method to the Debug class. This returns a backtrace array that is simpler and more PW-specific than PHP's version. By default it excludes likely irrelevant (for most) hook-related method calls that usually fill up the backtrace. --- wire/core/Debug.php | 230 ++++++++++++++++++++++++++++++++++++++ wire/core/Notices.php | 2 +- wire/core/ProcessWire.php | 7 +- 3 files changed, 235 insertions(+), 4 deletions(-) diff --git a/wire/core/Debug.php b/wire/core/Debug.php index cc3b72ec..f2b14b0e 100644 --- a/wire/core/Debug.php +++ b/wire/core/Debug.php @@ -154,4 +154,234 @@ class Debug { self::$timers = array(); } + /** + * Return a backtrace array that is simpler and more PW-specific relative to PHP’s debug_backtrace + * + * @param array $options + * @return array|string + * @since 3.0.136 + * + */ + static public function backtrace(array $options = array()) { + + $defaults = array( + 'limit' => 0, // the limit argument for the debug_backtrace call + 'flags' => DEBUG_BACKTRACE_PROVIDE_OBJECT, // flags for PHP debug_backtrace method + 'showHooks' => false, // show internal methods for hook calls? + 'getString' => false, // get newline separated string rather than array? + 'maxCount' => 10, // max size for arrays + 'maxStrlen' => 100, // max length for strings + 'maxDepth' => 5, // max allowed recursion depth when converting variables to strings + 'ellipsis' => ' …', // show this ellipsis when a long value is truncated + 'skipCalls' => array(), // method/function calls to skip + ); + + $options = array_merge($defaults, $options); + if($options['limit']) $options['limit']++; + $traces = @debug_backtrace($options['flags'], $options['limit']); + $config = wire('config'); + $rootPath = ProcessWire::getRootPath(true); + $rootPath2 = $config && $config->paths ? $config->paths->root : $rootPath; + array_shift($traces); // shift of the simpleBacktrace call, which is not needed + $apiVars = array(); + $result = array(); + $cnt = 0; + + foreach(wire('all') as $name => $value) { + if(!is_object($value)) continue; + $apiVars[wireClassName($value)] = '$' . $name; + } + + foreach($traces as $n => $trace) { + + if(!is_array($trace) || !isset($trace['function']) || !isset($trace['file'])) { + continue; + } else if(count($options['skipCalls']) && in_array($trace['function'], $options['skipCalls'])) { + continue; + } + + $obj = null; + $class = ''; + $type = ''; + $args = $trace['args']; + $argStr = ''; + $file = $trace['file']; + $basename = basename($file); + $function = $trace['function']; + $isHookableCall = false; + + if(isset($trace['object'])) { + $obj = $trace['object']; + $class = wireClassName($obj); + } else if(isset($trace['class'])) { + $class = wireClassName($trace['class']); + } + + if($class) { + $type = isset($trace['type']) ? $trace['type'] : '.'; + } + + if(!$options['showHooks']) { + if($basename === 'Wire.php' && !wireMethodExists('Wire', $function)) continue; + if($class === 'WireHooks' || $basename === 'WireHooks.php') continue; + } + + if(strpos($function, '___') === 0) { + $isHookableCall = '___'; + } else if($obj && !method_exists($obj, $function) && method_exists($obj, "___$function")) { + $isHookableCall = true; + } + + if($type === '->' && isset($apiVars[$class])) { + // use API var name when available + if(strtolower($class) === strtolower(ltrim($apiVars[$class], '$'))) { + $class = $apiVars[$class]; + } else { + $class = "$class " . $apiVars[$class]; + } + } + + if($basename === 'Wire.php' && $class !== 'Wire') { + $ref = new \ReflectionClass($trace['class']); + $file = $ref->getFileName(); + } + + // rootPath and rootPath2 can be different if one of them represented by a symlink + $file = str_replace($rootPath, '/', $file); + if($rootPath2 !== $rootPath) $file = str_replace($rootPath2, '/', $file); + + if(($function === '__call' || $function == '_callMethod') && count($args)) { + $function = array_shift($args); + } + + if(!$options['showHooks'] && $isHookableCall === '___') { + $function = substr($function, 3); + } + + if(!empty($args)) { + $newArgs = array(); + if($isHookableCall && count($args) === 1 && is_array($args[0])) { + $newArgs = $args[0]; + } + foreach($args as $arg) { + if(is_object($arg)) { + $arg = wireClassName($arg) . ' $obj'; + } else if(is_array($arg)) { + $count = count($arg); + if($count < 4) { + $arg = $count ? self::toStr($arg, array('maxDepth' => 2)) : '[]'; + } else { + $arg = 'array(' . count($arg) . ')'; + } + } else if(is_string($arg)) { + if(strlen("$arg") > $options['maxStrlen']) $arg = substr($arg, 0, $options['maxStrlen']) . ' …'; + $arg = '"' . $arg . '"'; + } else if(is_bool($arg)) { + $arg = $arg ? 'true' : 'false'; + } else { + // leave as-is + } + $newArgs[] = $arg; + } + $argStr = implode(', ', $newArgs); + if($argStr === '[]') $argStr = ''; + } + + $call = "$class$type$function($argStr)"; + $file = "$file:$trace[line]"; + + if($options['getString']) { + $result[] = "$cnt. $file » $call"; + } else { + $result[] = array( + 'file' => $file, + 'call' => $call, + ); + } + + $cnt++; + } + + if($options['getString']) $result = implode("\n", $result); + + return $result; + } + + /** + * Convert value to string for backtrace method + * + * @param $value + * @param array $options + * @return null|string + * + */ + static protected function toStr($value, array $options = array()) { + + $defaults = array( + 'maxCount' => 10, // max size for arrays + 'maxStrlen' => 100, // max length for strings + 'maxDepth' => 5, + 'ellipsis' => ' …' + ); + + static $depth = 0; + $options = count($options) ? array_merge($defaults, $options) : $defaults; + $depth++; + + if(is_object($value)) { + // object + $str = wireClassName($value); + if($str === 'HookEvent') { + $str .= ' $event'; + } else if(method_exists($value, '__toString')) { + $value = (string) $value; + if($value !== $str) { + if(strlen($value) > $options['maxStrlen']) { + $value = substr($value, 0, $options['maxStrlen']) . $options['ellipsis']; + } + $str .= "($value)"; + } + } + } else if(is_array($value)) { + // array + if(empty($value)) { + $str = '[]'; + } else if($depth >= $options['maxDepth']) { + $str = "array(" . count($value) . ")"; + } else { + $suffix = ''; + if(count($value) > $options['maxCount']) { + $value = array_slice($value, 0, $options['maxCount']); + $suffix = $options['ellipsis']; + } + foreach($value as $k => $v) { + $value[$k] = self::toStr($v, $options); + } + $str = '[ ' . implode(', ', $value) . $suffix . ' ]'; + } + } else if(is_string($value)) { + // string + if(strlen($value) > $options['maxStrlen']) { + $value = substr($value, 0, $options['maxStrlen']) . $options['ellipsis']; + } + $hasDQ = strpos($value, '"') !== false; + $hasSQ = strpos($value, "'") !== false; + if(($hasDQ && $hasSQ) || $hasSQ) { + $value = str_replace('"', '\\"', $value); + $str = '"' . $value . '"'; + } else { + $str = "'$value'"; + } + } else if(is_bool($value)) { + // true or false + $str = $value ? 'true' : 'false'; + } else { + // int, float or other + $str = $value; + } + + $depth--; + + return $str; + } } diff --git a/wire/core/Notices.php b/wire/core/Notices.php index 78ca0ab2..3443a7a8 100644 --- a/wire/core/Notices.php +++ b/wire/core/Notices.php @@ -90,7 +90,7 @@ abstract class Notice extends WireData { } public function set($key, $value) { - if($key === 'text' && strpos($value, 'icon-') === 0 && strpos($value, ' ')) { + if($key === 'text' && is_string($value) && strpos($value, 'icon-') === 0 && strpos($value, ' ')) { list($icon, $value) = explode(' ', $value, 2); list(,$icon) = explode('-', $icon, 2); $icon = $this->wire('sanitizer')->name($icon); diff --git a/wire/core/ProcessWire.php b/wire/core/ProcessWire.php index b0e66623..7453494d 100644 --- a/wire/core/ProcessWire.php +++ b/wire/core/ProcessWire.php @@ -760,12 +760,13 @@ class ProcessWire extends Wire { * Get root path, check it, and optionally auto-detect it if not provided * * @param bool|string $rootPath Root path if already known, in which case we’ll just modify as needed + * …or specify boolean true to get absolute root path, which disregards any symbolic links to core. * @return string * */ - protected static function getRootPath($rootPath = '') { + public static function getRootPath($rootPath = '') { - if(strpos($rootPath, '..') !== false) { + if($rootPath !== true && strpos($rootPath, '..') !== false) { $rootPath = realpath($rootPath); } @@ -777,7 +778,7 @@ class ProcessWire extends Wire { if(!file_exists($rootPath . 'wire/core/ProcessWire.php')) $rootPath = ''; } - if(empty($rootPath)) { + if(empty($rootPath) || $rootPath === true) { // if unable to determine from script filename, attempt to determine from current file $parts = explode(DIRECTORY_SEPARATOR, __FILE__); $parts = array_slice($parts, 0, -3); // removes "ProcessWire.php", "core" and "wire"