You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
616 lines
21 KiB
PHP
616 lines
21 KiB
PHP
<?php
|
|
|
|
/**
|
|
* @internal
|
|
*/
|
|
class SageParser
|
|
{
|
|
public static $_level = 0;
|
|
/** @var array<string, true> */
|
|
private static $_objects;
|
|
/** @var string */
|
|
private static $_marker;
|
|
|
|
private static $parsingAlternative = array();
|
|
|
|
private static $_placeFullStringInValue = false;
|
|
|
|
public static function reset()
|
|
{
|
|
self::$_level = 0;
|
|
self::$_objects = self::$_marker = null;
|
|
}
|
|
|
|
final public static function process(&$variable, $name = null)
|
|
{
|
|
// save internal data to revert after dumping to properly handle recursions etc
|
|
$revert = array(
|
|
'level' => self::$_level,
|
|
'objects' => self::$_objects,
|
|
);
|
|
|
|
self::$_level++;
|
|
|
|
// set name
|
|
$varData = new SageVariableData();
|
|
if (isset($name)) {
|
|
$varData->name = $name;
|
|
|
|
if (strlen($varData->name) > 60) {
|
|
$varData->name =
|
|
SageHelper::substr($varData->name, 0, 27)
|
|
. '...'
|
|
. SageHelper::substr($varData->name, -28, null);
|
|
}
|
|
}
|
|
|
|
// first go through alternative parsers (eg.: json detection)
|
|
foreach (Sage::$enabledParsers as $parserClass => $enabled) {
|
|
if (! $enabled || array_key_exists($parserClass, self::$parsingAlternative)) {
|
|
continue;
|
|
}
|
|
self::$parsingAlternative[$parserClass] = true;
|
|
$parser = new $parserClass();
|
|
$parseResult = $parser->parse($variable, $varData);
|
|
|
|
// if var was parsed by "can only be one"-parser - return here
|
|
if ($parseResult !== false && $parser->replacesAllOtherParsers()) {
|
|
unset(self::$parsingAlternative[$parserClass]);
|
|
self::$_level = $revert['level'];
|
|
self::$_objects = $revert['objects'];
|
|
|
|
return $varData;
|
|
}
|
|
unset(self::$parsingAlternative[$parserClass]);
|
|
}
|
|
|
|
// parse the variable based on its type
|
|
$varType = gettype($variable);
|
|
$varType === 'unknown type' and $varType = 'unknown'; // PHP 5.4 inconsistency
|
|
$methodName = '_parse_' . $varType;
|
|
if (! method_exists(__CLASS__, $methodName)) {
|
|
$varData->type = $varType; // resource (closed) for example
|
|
|
|
return $varData;
|
|
}
|
|
// base type parser returning false means "stop processing further": e.g. recursion
|
|
if (self::$methodName($variable, $varData) === false) {
|
|
self::$_level--;
|
|
|
|
return $varData;
|
|
}
|
|
|
|
self::$_level = $revert['level'];
|
|
self::$_objects = $revert['objects'];
|
|
|
|
return $varData;
|
|
}
|
|
|
|
private static function isDepthLimit()
|
|
{
|
|
return Sage::$maxLevels && self::$_level >= Sage::$maxLevels;
|
|
}
|
|
|
|
/**
|
|
* @return array|false return ALL keys from all rows if array is tabular, false otherwise
|
|
*/
|
|
private static function _isArrayTabular(array $variable)
|
|
{
|
|
if (Sage::enabled() !== Sage::MODE_RICH) {
|
|
return false;
|
|
}
|
|
|
|
$arrayKeys = array();
|
|
$keys = null;
|
|
$closeEnough = false;
|
|
foreach ($variable as $k => $row) {
|
|
if (isset(self::$_marker) && $k === self::$_marker) {
|
|
continue;
|
|
}
|
|
|
|
if (! is_array($row) || empty($row)) {
|
|
return false;
|
|
}
|
|
|
|
foreach ($row as $col) {
|
|
if (! empty($col) && ! is_scalar($col)) {
|
|
return false;
|
|
} // todo add tabular "tolerance"
|
|
}
|
|
|
|
if (isset($keys) && ! $closeEnough) {
|
|
// let's just see if the first two rows have same keys, that's faster and has the
|
|
// positive side effect of easily spotting missing keys in later rows
|
|
if ($keys !== array_keys($row)) {
|
|
return false;
|
|
}
|
|
|
|
$closeEnough = true;
|
|
} else {
|
|
$keys = array_keys($row);
|
|
}
|
|
|
|
$arrayKeys = array_unique(array_merge($arrayKeys, $keys));
|
|
}
|
|
|
|
return $arrayKeys;
|
|
}
|
|
|
|
private static function _decorateCell(SageVariableData $varData)
|
|
{
|
|
if ($varData->extendedValue !== null) {
|
|
return '<td>' . SageDecoratorsRich::decorate($varData) . '</td>';
|
|
}
|
|
|
|
$output = '<td';
|
|
|
|
if ($varData->value !== null) {
|
|
$output .= ' title="' . $varData->type;
|
|
|
|
if ($varData->size !== null) {
|
|
$output .= ' (' . $varData->size . ')';
|
|
}
|
|
|
|
$output .= '">' . $varData->value;
|
|
} else {
|
|
$output .= '>';
|
|
|
|
if ($varData->type !== 'NULL') {
|
|
$output .= '<u>' . $varData->type;
|
|
|
|
if ($varData->size !== null) {
|
|
$output .= '(' . $varData->size . ')';
|
|
}
|
|
|
|
$output .= '</u>';
|
|
} else {
|
|
$output .= '<u>NULL</u>';
|
|
}
|
|
}
|
|
|
|
return $output . '</td>';
|
|
}
|
|
|
|
private static $_dealingWithGlobals = false;
|
|
|
|
private static function _parse_array(&$variable, SageVariableData $variableData)
|
|
{
|
|
isset(self::$_marker) or self::$_marker = "\x00" . uniqid();
|
|
|
|
// naturally, $GLOBALS variable is an intertwined recursion nightmare, use black magic
|
|
$globalsDetector = false;
|
|
if (array_key_exists('GLOBALS', $variable) && is_array($variable['GLOBALS'])) {
|
|
$globalsDetector = "\x01" . uniqid();
|
|
|
|
$variable['GLOBALS'][$globalsDetector] = true;
|
|
if (isset($variable[$globalsDetector])) {
|
|
unset($variable[$globalsDetector]);
|
|
self::$_dealingWithGlobals = true;
|
|
} else {
|
|
unset($variable['GLOBALS'][$globalsDetector]);
|
|
$globalsDetector = false;
|
|
}
|
|
}
|
|
|
|
$variableData->type = 'array';
|
|
$variableData->size = count($variable);
|
|
|
|
if ($variableData->size === 0) {
|
|
return;
|
|
}
|
|
if (isset($variable[self::$_marker])) { // recursion; todo mayhaps show from where
|
|
if (self::$_dealingWithGlobals) {
|
|
$variableData->value = '*RECURSION*';
|
|
} else {
|
|
unset($variable[self::$_marker]);
|
|
$variableData->value = self::$_marker;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
if (self::isDepthLimit()) {
|
|
$variableData->extendedValue = '*DEPTH TOO GREAT*';
|
|
|
|
return false;
|
|
}
|
|
|
|
$isSequential = SageHelper::isArraySequential($variable);
|
|
$variable[self::$_marker] = true;
|
|
|
|
if ($variableData->size > 1 && ($arrayKeys = self::_isArrayTabular($variable)) !== false) {
|
|
// tabular array parse
|
|
$firstRow = true;
|
|
$extendedValue = '<table class="_sage-report"><thead>';
|
|
|
|
foreach ($variable as $rowIndex => & $row) {
|
|
// display strings in their full length
|
|
self::$_placeFullStringInValue = true;
|
|
|
|
if ($rowIndex === self::$_marker) {
|
|
continue;
|
|
}
|
|
|
|
if (isset($row[self::$_marker])) {
|
|
$variableData->value = '*RECURSION*';
|
|
|
|
return false;
|
|
}
|
|
|
|
$extendedValue .= '<tr>';
|
|
if ($isSequential) {
|
|
$output = '<td>' . (((int)$rowIndex) + 1) . '</td>';
|
|
} else {
|
|
$output = self::_decorateCell(self::process($rowIndex));
|
|
}
|
|
if ($firstRow) {
|
|
$extendedValue .= '<th> </th>';
|
|
}
|
|
|
|
// we iterate the known full set of keys from all rows in case some appeared at later rows,
|
|
// as we only check the first two to assume
|
|
foreach ($arrayKeys as $key) {
|
|
if ($firstRow) {
|
|
$extendedValue .= '<th>' . SageHelper::esc($key) . '</th>';
|
|
}
|
|
|
|
if (SageHelper::isKeyBlacklisted($key)) {
|
|
$output .= '<td class="_sage-empty"><u>*REDACTED*</u></td>';
|
|
continue;
|
|
}
|
|
|
|
if (! array_key_exists($key, $row)) {
|
|
$output .= '<td class="_sage-empty"></td>';
|
|
continue;
|
|
}
|
|
|
|
$var = self::process($row[$key]);
|
|
|
|
if ($var->value === self::$_marker) {
|
|
$variableData->value = '*RECURSION*';
|
|
|
|
return false;
|
|
} elseif ($var->value === '*RECURSION*') {
|
|
$output .= '<td class="_sage-empty"><u>*RECURSION*</u></td>';
|
|
} else {
|
|
$output .= self::_decorateCell($var);
|
|
}
|
|
unset($var);
|
|
}
|
|
|
|
if ($firstRow) {
|
|
$extendedValue .= '</tr></thead><tr>';
|
|
$firstRow = false;
|
|
}
|
|
|
|
$extendedValue .= $output . '</tr>';
|
|
}
|
|
self::$_placeFullStringInValue = false;
|
|
|
|
$variableData->extendedValue = $extendedValue . '</table>';
|
|
} else {
|
|
$extendedValue = array();
|
|
|
|
foreach ($variable as $key => & $val) {
|
|
if ($key === self::$_marker) {
|
|
continue;
|
|
}
|
|
|
|
if (in_array($key, Sage::$keysBlacklist, true)) {
|
|
$val = '*REDACTED*';
|
|
}
|
|
|
|
$output = self::process($val);
|
|
if ($output->value === self::$_marker) {
|
|
// recursion occurred on a higher level, thus $variableData is recursion
|
|
$variableData->value = '*RECURSION*';
|
|
|
|
return false;
|
|
}
|
|
if ($isSequential) {
|
|
$output->name = null;
|
|
} else {
|
|
$output->operator = '=>';
|
|
$output->name = is_int($key)
|
|
? $key
|
|
: "'" . $key . "'";
|
|
}
|
|
$extendedValue[] = $output;
|
|
}
|
|
$variableData->extendedValue = $extendedValue;
|
|
}
|
|
|
|
if ($globalsDetector) {
|
|
self::$_dealingWithGlobals = false;
|
|
}
|
|
|
|
unset($variable[self::$_marker]);
|
|
}
|
|
|
|
private static function _parse_object(&$variable, SageVariableData $variableData)
|
|
{
|
|
$hash = self::getObjectHash($variable);
|
|
|
|
$castedArray = (array)$variable;
|
|
$className = get_class($variable);
|
|
$variableData->type = $className;
|
|
$variableData->size = count($castedArray);
|
|
|
|
if (isset(self::$_objects[$hash])) {
|
|
$variableData->value = '*RECURSION*';
|
|
|
|
return false;
|
|
}
|
|
if (self::isDepthLimit()) {
|
|
$variableData->extendedValue = '*DEPTH TOO GREAT*';
|
|
|
|
return false;
|
|
}
|
|
|
|
// ArrayObject (and maybe ArrayIterator, did not try yet) unsurprisingly consist of mainly dark magic.
|
|
// What bothers me most, var_dump sees no problem with it, and ArrayObject also uses a custom,
|
|
// undocumented serialize function, so you can see the properties in internal functions, but
|
|
// can never iterate some of them if the flags are not STD_PROP_LIST. Fun stuff.
|
|
if ($variableData->type === 'ArrayObject' || is_subclass_of($variable, 'ArrayObject')) {
|
|
$arrayObjectFlags = $variable->getFlags();
|
|
$variable->setFlags(ArrayObject::STD_PROP_LIST);
|
|
}
|
|
|
|
if (strpos($variableData->type, "@anonymous\0") !== false) {
|
|
$variableData->type = 'Anonymous class';
|
|
}
|
|
|
|
self::$_objects[$hash] = true;
|
|
$variableReflection = new ReflectionObject($variable);
|
|
|
|
// add link to definition of userland objects
|
|
if (SageHelper::isHtmlMode() && $variableReflection->isUserDefined()) {
|
|
$variableData->type = SageHelper::ideLink(
|
|
$variableReflection->getFileName(),
|
|
$variableReflection->getStartLine(),
|
|
$variableData->type
|
|
);
|
|
}
|
|
$variableData->size = 0;
|
|
|
|
$extendedValue = array();
|
|
static $publicProperties = array();
|
|
if (! isset($publicProperties[$className])) {
|
|
$reflectionClass = new ReflectionClass($className);
|
|
foreach ($reflectionClass->getProperties(ReflectionProperty::IS_PUBLIC) as $prop) {
|
|
$publicProperties[$className][$prop->name] = true;
|
|
}
|
|
}
|
|
|
|
// copy the object as an array as it provides more info than Reflection (depends)
|
|
foreach ($castedArray as $key => $value) {
|
|
/* casting object to array:
|
|
* integer properties are inaccessible;
|
|
* private variables have the class name prepended to the variable name;
|
|
* protected variables have a '*' prepended to the variable name.
|
|
* These prepended values have null bytes on either side.
|
|
* http://www.php.net/manual/en/language.types.array.php#language.types.array.casting
|
|
*/
|
|
if (is_string($key) && $key[0] === "\x00") {
|
|
$access = $key[1] === '*' ? 'protected' : 'private';
|
|
|
|
// Remove the access level from the variable name
|
|
$key = substr($key, strrpos($key, "\x00") + 1);
|
|
} else {
|
|
$access = 'public';
|
|
|
|
if ($variableData->type !== 'stdClass' && ! isset($publicProperties[$className][$key])) {
|
|
$access .= ' (dynamically added)';
|
|
}
|
|
}
|
|
|
|
if (SageHelper::isKeyBlacklisted($key)) {
|
|
$value = '*REDACTED*';
|
|
}
|
|
|
|
$output = self::process($value);
|
|
$output->name = SageHelper::esc($key);
|
|
$output->access = $access;
|
|
$output->operator = '->';
|
|
|
|
$extendedValue[$key] = $output;
|
|
|
|
$variableData->size++;
|
|
}
|
|
|
|
if ($variable instanceof __PHP_Incomplete_Class) {
|
|
$variableData->extendedValue = $extendedValue;
|
|
|
|
return $castedArray;
|
|
}
|
|
|
|
foreach ($variableReflection->getProperties() as $property) {
|
|
if ($property->isStatic()) {
|
|
continue;
|
|
}
|
|
|
|
$name = $property->getName();
|
|
if (isset($extendedValue[$name])) {
|
|
if (method_exists($property, 'isReadOnly') && $property->isReadOnly()) {
|
|
$extendedValue[$name]->access .= ' readonly';
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
if ($property->isProtected()) {
|
|
$property->setAccessible(true);
|
|
$access = 'protected';
|
|
} elseif ($property->isPrivate()) {
|
|
$property->setAccessible(true);
|
|
$access = 'private';
|
|
} else {
|
|
$access = 'public';
|
|
}
|
|
|
|
if (method_exists($property, 'isInitialized')
|
|
&& ! $property->isInitialized($variable)) {
|
|
$value = null;
|
|
$access .= ' [uninitialized]';
|
|
} else {
|
|
$value = $property->getValue($variable);
|
|
}
|
|
|
|
$output = self::process($value, SageHelper::esc($name));
|
|
|
|
$output->access = $access;
|
|
$output->operator = '->';
|
|
$extendedValue[] = $output;
|
|
$variableData->size++;
|
|
}
|
|
|
|
if (isset($arrayObjectFlags)) {
|
|
$variable->setFlags($arrayObjectFlags);
|
|
}
|
|
|
|
if (method_exists($variableReflection, 'isEnum') && $variableReflection->isEnum()) {
|
|
$variableData->size = 'enum';
|
|
$variableData->value = '"' . $variable->name . '"';
|
|
}
|
|
|
|
if ($variableData->size) {
|
|
$variableData->extendedValue = $extendedValue;
|
|
}
|
|
}
|
|
|
|
private static function _parse_boolean(&$variable, SageVariableData $variableData)
|
|
{
|
|
$variableData->type = 'bool';
|
|
$variableData->value = $variable ? 'TRUE' : 'FALSE';
|
|
}
|
|
|
|
private static function _parse_double(&$variable, SageVariableData $variableData)
|
|
{
|
|
$variableData->type = 'float';
|
|
$variableData->value = $variable;
|
|
}
|
|
|
|
private static function _parse_integer(&$variable, SageVariableData $variableData)
|
|
{
|
|
$variableData->type = 'integer';
|
|
$variableData->value = $variable;
|
|
}
|
|
|
|
private static function _parse_null(&$variable, SageVariableData $variableData)
|
|
{
|
|
$variableData->type = 'NULL';
|
|
}
|
|
|
|
private static function _parse_resource(&$variable, SageVariableData $variableData)
|
|
{
|
|
$resourceType = get_resource_type($variable);
|
|
$variableData->type = "resource ({$resourceType})";
|
|
|
|
if ($resourceType === 'stream' && $meta = stream_get_meta_data($variable)) {
|
|
if (isset($meta['uri'])) {
|
|
$file = $meta['uri'];
|
|
|
|
if (function_exists('stream_is_local')) {
|
|
// Only exists on PHP >= 5.2.4
|
|
if (stream_is_local($file)) {
|
|
$file = SageHelper::shortenPath($file);
|
|
}
|
|
}
|
|
|
|
$variableData->value = $file;
|
|
}
|
|
}
|
|
}
|
|
|
|
private static function _parse_string(&$variable, SageVariableData $variableData)
|
|
{
|
|
if (preg_match('//u', $variable)) {
|
|
$variableData->type = 'string';
|
|
} else {
|
|
$variableData->type .= 'binary string';
|
|
}
|
|
|
|
$encoding = SageHelper::detectEncoding($variable);
|
|
if ($encoding !== 'UTF-8') {
|
|
$variableData->type .= ' ' . $encoding;
|
|
}
|
|
|
|
$variableData->size = SageHelper::strlen($variable, $encoding);
|
|
|
|
if (self::$_placeFullStringInValue) { // in tabular view
|
|
$variableData->value = SageHelper::esc($variable);
|
|
} elseif (! SageHelper::isRichMode()) {
|
|
$variableData->value = '"' . SageHelper::esc($variable) . '"';
|
|
} else {
|
|
$decoded = SageHelper::esc($variable);
|
|
|
|
// trim inline value if too long
|
|
if ($variableData->size > (SageHelper::MAX_STR_LENGTH + 8)) {
|
|
$variableData->value =
|
|
'"'
|
|
. SageHelper::esc(
|
|
SageHelper::substr($variable, 0, SageHelper::MAX_STR_LENGTH, $encoding),
|
|
false
|
|
)
|
|
. '…"';
|
|
} else {
|
|
$variableData->value = '"' . SageHelper::esc($variable, false) . '"';
|
|
}
|
|
|
|
// detect invisible characters
|
|
if ($variable !== preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x80-\x9F]/u', '', $variable)) {
|
|
$variableData->extendedValue = SageHelper::esc($variable, false);
|
|
$variableData->addTabToView($variable, 'Hidden characters escaped', $decoded);
|
|
} elseif ($variableData->size > (SageHelper::MAX_STR_LENGTH + 8)
|
|
|| $variable !== preg_replace(
|
|
'/\s+/',
|
|
' ',
|
|
$variable
|
|
)) {
|
|
$variableData->extendedValue = $decoded;
|
|
}
|
|
}
|
|
}
|
|
|
|
private static function _parse_unknown(&$variable, SageVariableData $variableData)
|
|
{
|
|
$type = gettype($variable);
|
|
$variableData->type = 'UNKNOWN' . (! empty($type) ? " ({$type})" : '');
|
|
$variableData->value = var_export($variable, true);
|
|
}
|
|
|
|
private static function getObjectHash($variable)
|
|
{
|
|
if (function_exists('spl_object_hash')) { // since PHP 5.2
|
|
return spl_object_hash($variable);
|
|
}
|
|
|
|
ob_start();
|
|
var_dump($variable);
|
|
preg_match('[#(\d+)]', ob_get_clean(), $match);
|
|
|
|
return $match[1];
|
|
}
|
|
|
|
public static function alternativesParse($originalVar, $alternativesArray)
|
|
{
|
|
$varData = new SageVariableData();
|
|
|
|
// if the alternatives contain references to the object itself it's an ∞ loop
|
|
if (is_object($originalVar)) {
|
|
self::$_objects[self::getObjectHash($originalVar)] = true;
|
|
} elseif (is_array($originalVar)) {
|
|
isset(self::$_marker) or self::$_marker = "\x00" . uniqid();
|
|
|
|
$originalVar[self::$_marker] = true;
|
|
}
|
|
|
|
if (is_array($alternativesArray)) {
|
|
self::_parse_array($alternativesArray, $varData);
|
|
} else {
|
|
self::_parse_object($alternativesArray, $varData);
|
|
}
|
|
|
|
return $varData->extendedValue;
|
|
}
|
|
}
|