kintParser.class.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608
  1. <?php
  2. abstract class kintParser extends kintVariableData
  3. {
  4. private static $_level = 0;
  5. private static $_customDataTypes;
  6. private static $_objectParsers;
  7. private static $_objects;
  8. private static $_marker;
  9. private static $_skipAlternatives = false;
  10. private static $_placeFullStringInValue = false;
  11. private static function _init()
  12. {
  13. $fh = opendir( KINT_DIR . 'parsers/custom/' );
  14. while ( $fileName = readdir( $fh ) ) {
  15. if ( substr( $fileName, -4 ) !== '.php' ) continue;
  16. require KINT_DIR . 'parsers/custom/' . $fileName;
  17. self::$_customDataTypes[] = substr( $fileName, 0, -4 );
  18. }
  19. $fh = opendir( KINT_DIR . 'parsers/objects/' );
  20. while ( $fileName = readdir( $fh ) ) {
  21. if ( substr( $fileName, -4 ) !== '.php' ) continue;
  22. require KINT_DIR . 'parsers/objects/' . $fileName;
  23. self::$_objectParsers[] = substr( $fileName, 0, -4 );
  24. }
  25. }
  26. public static function reset()
  27. {
  28. self::$_level = 0;
  29. self::$_objects = self::$_marker = null;
  30. }
  31. /**
  32. * main and usually single method a custom parser must implement
  33. *
  34. * @param mixed $variable
  35. *
  36. * @return mixed [!!!] false is returned if the variable is not of current type
  37. */
  38. abstract protected function _parse( & $variable );
  39. /**
  40. * the only public entry point to return a parsed representation of a variable
  41. *
  42. * @static
  43. *
  44. * @param $variable
  45. * @param null $name
  46. *
  47. * @throws Exception
  48. * @return \kintParser
  49. */
  50. public final static function factory( & $variable, $name = null )
  51. {
  52. isset( self::$_customDataTypes ) or self::_init();
  53. # save internal data to revert after dumping to properly handle recursions etc
  54. $revert = array(
  55. 'level' => self::$_level,
  56. 'objects' => self::$_objects,
  57. );
  58. self::$_level++;
  59. $varData = new kintVariableData;
  60. $varData->name = $name;
  61. # first parse the variable based on its type
  62. $varType = gettype( $variable );
  63. $varType === 'unknown type' and $varType = 'unknown'; # PHP 5.4 inconsistency
  64. $methodName = '_parse_' . $varType;
  65. # objects can be presented in a different way altogether, INSTEAD, not ALONGSIDE the generic parser
  66. if ( $varType === 'object' ) {
  67. foreach ( self::$_objectParsers as $parserClass ) {
  68. $className = 'Kint_Objects_' . $parserClass;
  69. /** @var $object KintObject */
  70. $object = new $className;
  71. if ( ( $alternativeTabs = $object->parse( $variable ) ) !== false ) {
  72. self::$_skipAlternatives = true;
  73. $alternativeDisplay = new kintVariableData;
  74. $alternativeDisplay->type = $object->name;
  75. $alternativeDisplay->value = $object->value;
  76. $alternativeDisplay->name = $name;
  77. foreach ( $alternativeTabs as $name => $values ) {
  78. $alternative = kintParser::factory( $values );
  79. $alternative->type = $name;
  80. if ( Kint::enabled() === Kint::MODE_RICH ) {
  81. empty( $alternative->value ) and $alternative->value = $alternative->extendedValue;
  82. $alternativeDisplay->_alternatives[] = $alternative;
  83. } else {
  84. $alternativeDisplay->extendedValue[] = $alternative;
  85. }
  86. }
  87. self::$_skipAlternatives = false;
  88. self::$_level = $revert['level'];
  89. self::$_objects = $revert['objects'];
  90. return $alternativeDisplay;
  91. }
  92. }
  93. }
  94. # base type parser returning false means "stop processing further": e.g. recursion
  95. if ( self::$methodName( $variable, $varData ) === false ) {
  96. self::$_level--;
  97. return $varData;
  98. }
  99. if ( Kint::enabled() === Kint::MODE_RICH && !self::$_skipAlternatives ) {
  100. # if an alternative returns something that can be represented in an alternative way, don't :)
  101. self::$_skipAlternatives = true;
  102. # now check whether the variable can be represented in a different way
  103. foreach ( self::$_customDataTypes as $parserClass ) {
  104. $className = 'Kint_Parsers_' . $parserClass;
  105. /** @var $parser kintParser */
  106. $parser = new $className;
  107. $parser->name = $name; # the parser may overwrite the name value, so set it first
  108. if ( $parser->_parse( $variable ) !== false ) {
  109. $varData->_alternatives[] = $parser;
  110. }
  111. }
  112. # if alternatives exist, push extendedValue to their front and display it as one of alternatives
  113. if ( !empty( $varData->_alternatives ) && isset( $varData->extendedValue ) ) {
  114. $_ = new kintVariableData;
  115. $_->value = $varData->extendedValue;
  116. $_->type = 'contents';
  117. $_->size = null;
  118. array_unshift( $varData->_alternatives, $_ );
  119. $varData->extendedValue = null;
  120. }
  121. self::$_skipAlternatives = false;
  122. }
  123. self::$_level = $revert['level'];
  124. self::$_objects = $revert['objects'];
  125. if ( strlen( $varData->name ) > 80 ) {
  126. $varData->name =
  127. self::_substr( $varData->name, 0, 37 )
  128. . '...'
  129. . self::_substr( $varData->name, -38, null );
  130. }
  131. return $varData;
  132. }
  133. private static function _checkDepth()
  134. {
  135. return Kint::$maxLevels != 0 && self::$_level >= Kint::$maxLevels;
  136. }
  137. private static function _isArrayTabular( array $variable )
  138. {
  139. if ( Kint::enabled() !== Kint::MODE_RICH ) return false;
  140. $arrayKeys = array();
  141. $keys = null;
  142. $closeEnough = false;
  143. foreach ( $variable as $row ) {
  144. if ( !is_array( $row ) || empty( $row ) ) return false;
  145. foreach ( $row as $col ) {
  146. if ( !empty( $col ) && !is_scalar( $col ) ) return false; // todo add tabular "tolerance"
  147. }
  148. if ( isset( $keys ) && !$closeEnough ) {
  149. # let's just see if the first two rows have same keys, that's faster and has the
  150. # positive side effect of easily spotting missing keys in later rows
  151. if ( $keys !== array_keys( $row ) ) return false;
  152. $closeEnough = true;
  153. } else {
  154. $keys = array_keys( $row );
  155. }
  156. $arrayKeys = array_unique( array_merge( $arrayKeys, $keys ) );
  157. }
  158. return $arrayKeys;
  159. }
  160. private static function _decorateCell( kintVariableData $kintVar )
  161. {
  162. if ( $kintVar->extendedValue !== null || !empty( $kintVar->_alternatives ) ) {
  163. return '<td>' . Kint_Decorators_Rich::decorate( $kintVar ) . '</td>';
  164. }
  165. $output = '<td';
  166. if ( $kintVar->value !== null ) {
  167. $output .= ' title="' . $kintVar->type;
  168. if ( $kintVar->size !== null ) {
  169. $output .= " (" . $kintVar->size . ")";
  170. }
  171. $output .= '">' . $kintVar->value;
  172. } else {
  173. $output .= '>';
  174. if ( $kintVar->type !== 'NULL' ) {
  175. $output .= '<u>' . $kintVar->type;
  176. if ( $kintVar->size !== null ) {
  177. $output .= "(" . $kintVar->size . ")";
  178. }
  179. $output .= '</u>';
  180. } else {
  181. $output .= '<u>NULL</u>';
  182. }
  183. }
  184. return $output . '</td>';
  185. }
  186. public static function escape( $value, $encoding = null )
  187. {
  188. if ( empty( $value ) ) return $value;
  189. if ( Kint::enabled() === Kint::MODE_CLI ) {
  190. $value = str_replace( "\x1b", "\\x1b", $value );
  191. }
  192. if ( Kint::enabled() === Kint::MODE_CLI || Kint::enabled() === Kint::MODE_WHITESPACE ) return $value;
  193. $encoding or $encoding = self::_detectEncoding( $value );
  194. $value = htmlspecialchars( $value, ENT_NOQUOTES, $encoding === 'ASCII' ? 'UTF-8' : $encoding );
  195. if ( $encoding === 'UTF-8' ) {
  196. // todo we could make the symbols hover-title show the code for the invisible symbol
  197. # when possible force invisible characters to have some sort of display (experimental)
  198. $value = preg_replace( '/[\x00-\x08\x0B\x0C\x0E-\x1F\x80-\x9F]/u', '?', $value );
  199. }
  200. # this call converts all non-ASCII characters into html chars of format
  201. if ( function_exists( 'mb_encode_numericentity' ) ) {
  202. $value = mb_encode_numericentity(
  203. $value,
  204. array( 0x80, 0xffff, 0, 0xffff, ),
  205. $encoding
  206. );
  207. }
  208. return $value;
  209. }
  210. private static $_dealingWithGlobals = false;
  211. private static function _parse_array( &$variable, kintVariableData $variableData )
  212. {
  213. isset( self::$_marker ) or self::$_marker = "\x00" . uniqid();
  214. # naturally, $GLOBALS variable is an intertwined recursion nightmare, use black magic
  215. $globalsDetector = false;
  216. if ( array_key_exists( 'GLOBALS', $variable ) && is_array( $variable['GLOBALS'] ) ) {
  217. $globalsDetector = "\x01" . uniqid();
  218. $variable['GLOBALS'][ $globalsDetector ] = true;
  219. if ( isset( $variable[ $globalsDetector ] ) ) {
  220. unset( $variable[ $globalsDetector ] );
  221. self::$_dealingWithGlobals = true;
  222. } else {
  223. unset( $variable['GLOBALS'][ $globalsDetector ] );
  224. $globalsDetector = false;
  225. }
  226. }
  227. $variableData->type = 'array';
  228. $variableData->size = count( $variable );
  229. if ( $variableData->size === 0 ) {
  230. return;
  231. }
  232. if ( isset( $variable[ self::$_marker ] ) ) { # recursion; todo mayhaps show from where
  233. if ( self::$_dealingWithGlobals ) {
  234. $variableData->value = '*RECURSION*';
  235. } else {
  236. unset( $variable[ self::$_marker ] );
  237. $variableData->value = self::$_marker;
  238. }
  239. return false;
  240. }
  241. if ( self::_checkDepth() ) {
  242. $variableData->extendedValue = "*DEPTH TOO GREAT*";
  243. return false;
  244. }
  245. $isSequential = self::_isSequential( $variable );
  246. if ( $variableData->size > 1 && ( $arrayKeys = self::_isArrayTabular( $variable ) ) !== false ) {
  247. $variable[ self::$_marker ] = true; # this must be AFTER _isArrayTabular
  248. $firstRow = true;
  249. $extendedValue = '<table class="kint-report"><thead>';
  250. foreach ( $variable as $rowIndex => & $row ) {
  251. # display strings in their full length
  252. self::$_placeFullStringInValue = true;
  253. if ( $rowIndex === self::$_marker ) continue;
  254. if ( isset( $row[ self::$_marker ] ) ) {
  255. $variableData->value = "*RECURSION*";
  256. return false;
  257. }
  258. $extendedValue .= '<tr>';
  259. if ( $isSequential ) {
  260. $output = '<td>' . '#' . ( $rowIndex + 1 ) . '</td>';
  261. } else {
  262. $output = self::_decorateCell( kintParser::factory( $rowIndex ) );
  263. }
  264. if ( $firstRow ) {
  265. $extendedValue .= '<th>&nbsp;</th>';
  266. }
  267. # we iterate the known full set of keys from all rows in case some appeared at later rows,
  268. # as we only check the first two to assume
  269. foreach ( $arrayKeys as $key ) {
  270. if ( $firstRow ) {
  271. $extendedValue .= '<th>' . self::escape( $key ) . '</th>';
  272. }
  273. if ( !array_key_exists( $key, $row ) ) {
  274. $output .= '<td class="kint-empty"></td>';
  275. continue;
  276. }
  277. $var = kintParser::factory( $row[ $key ] );
  278. if ( $var->value === self::$_marker ) {
  279. $variableData->value = '*RECURSION*';
  280. return false;
  281. } elseif ( $var->value === '*RECURSION*' ) {
  282. $output .= '<td class="kint-empty"><u>*RECURSION*</u></td>';
  283. } else {
  284. $output .= self::_decorateCell( $var );
  285. }
  286. unset( $var );
  287. }
  288. if ( $firstRow ) {
  289. $extendedValue .= '</tr></thead><tr>';
  290. $firstRow = false;
  291. }
  292. $extendedValue .= $output . '</tr>';
  293. }
  294. self::$_placeFullStringInValue = false;
  295. $variableData->extendedValue = $extendedValue . '</table>';
  296. } else {
  297. $variable[ self::$_marker ] = true;
  298. $extendedValue = array();
  299. foreach ( $variable as $key => & $val ) {
  300. if ( $key === self::$_marker ) continue;
  301. $output = kintParser::factory( $val );
  302. if ( $output->value === self::$_marker ) {
  303. $variableData->value = "*RECURSION*"; // recursion occurred on a higher level, thus $this is recursion
  304. return false;
  305. }
  306. if ( !$isSequential ) {
  307. $output->operator = '=>';
  308. }
  309. $output->name = $isSequential ? null : "'" . $key . "'";
  310. $extendedValue[] = $output;
  311. }
  312. $variableData->extendedValue = $extendedValue;
  313. }
  314. if ( $globalsDetector ) {
  315. self::$_dealingWithGlobals = false;
  316. }
  317. unset( $variable[ self::$_marker ] );
  318. }
  319. private static function _parse_object( &$variable, kintVariableData $variableData )
  320. {
  321. if ( function_exists( 'spl_object_hash' ) ) {
  322. $hash = spl_object_hash( $variable );
  323. } else {
  324. ob_start();
  325. var_dump( $variable );
  326. preg_match( '[#(\d+)]', ob_get_clean(), $match );
  327. $hash = $match[1];
  328. }
  329. $castedArray = (array) $variable;
  330. $variableData->type = get_class( $variable );
  331. $variableData->size = count( $castedArray );
  332. if ( isset( self::$_objects[ $hash ] ) ) {
  333. $variableData->value = '*RECURSION*';
  334. return false;
  335. }
  336. if ( self::_checkDepth() ) {
  337. $variableData->extendedValue = "*DEPTH TOO GREAT*";
  338. return false;
  339. }
  340. # ArrayObject (and maybe ArrayIterator, did not try yet) unsurprisingly consist of mainly dark magic.
  341. # What bothers me most, var_dump sees no problem with it, and ArrayObject also uses a custom,
  342. # undocumented serialize function, so you can see the properties in internal functions, but
  343. # can never iterate some of them if the flags are not STD_PROP_LIST. Fun stuff.
  344. if ( $variableData->type === 'ArrayObject' || is_subclass_of( $variable, 'ArrayObject' ) ) {
  345. $arrayObjectFlags = $variable->getFlags();
  346. $variable->setFlags( ArrayObject::STD_PROP_LIST );
  347. }
  348. self::$_objects[ $hash ] = true; // todo store reflectorObject here for alternatives cache
  349. $reflector = new ReflectionObject( $variable );
  350. # add link to definition of userland objects
  351. if ( Kint::enabled() === Kint::MODE_RICH && Kint::$fileLinkFormat && $reflector->isUserDefined() ) {
  352. $url = Kint::getIdeLink( $reflector->getFileName(), $reflector->getStartLine() );
  353. $class = ( strpos( $url, 'http://' ) === 0 ) ? 'class="kint-ide-link" ' : '';
  354. $variableData->type = "<a {$class}href=\"{$url}\">{$variableData->type}</a>";
  355. }
  356. $variableData->size = 0;
  357. $extendedValue = array();
  358. $encountered = array();
  359. # copy the object as an array as it provides more info than Reflection (depends)
  360. foreach ( $castedArray as $key => $value ) {
  361. /* casting object to array:
  362. * integer properties are inaccessible;
  363. * private variables have the class name prepended to the variable name;
  364. * protected variables have a '*' prepended to the variable name.
  365. * These prepended values have null bytes on either side.
  366. * http://www.php.net/manual/en/language.types.array.php#language.types.array.casting
  367. */
  368. if ( $key{0} === "\x00" ) {
  369. $access = $key{1} === "*" ? "protected" : "private";
  370. // Remove the access level from the variable name
  371. $key = substr( $key, strrpos( $key, "\x00" ) + 1 );
  372. } else {
  373. $access = "public";
  374. }
  375. $encountered[ $key ] = true;
  376. $output = kintParser::factory( $value, self::escape( $key ) );
  377. $output->access = $access;
  378. $output->operator = '->';
  379. $extendedValue[] = $output;
  380. $variableData->size++;
  381. }
  382. foreach ( $reflector->getProperties() as $property ) {
  383. $name = $property->name;
  384. if ( $property->isStatic() || isset( $encountered[ $name ] ) ) continue;
  385. if ( $property->isProtected() ) {
  386. $property->setAccessible( true );
  387. $access = "protected";
  388. } elseif ( $property->isPrivate() ) {
  389. $property->setAccessible( true );
  390. $access = "private";
  391. } else {
  392. $access = "public";
  393. }
  394. $value = $property->getValue( $variable );
  395. $output = kintParser::factory( $value, self::escape( $name ) );
  396. $output->access = $access;
  397. $output->operator = '->';
  398. $extendedValue[] = $output;
  399. $variableData->size++;
  400. }
  401. if ( isset( $arrayObjectFlags ) ) {
  402. $variable->setFlags( $arrayObjectFlags );
  403. }
  404. if ( $variableData->size ) {
  405. $variableData->extendedValue = $extendedValue;
  406. }
  407. }
  408. private static function _parse_boolean( &$variable, kintVariableData $variableData )
  409. {
  410. $variableData->type = 'bool';
  411. $variableData->value = $variable ? 'TRUE' : 'FALSE';
  412. }
  413. private static function _parse_double( &$variable, kintVariableData $variableData )
  414. {
  415. $variableData->type = 'float';
  416. $variableData->value = $variable;
  417. }
  418. private static function _parse_integer( &$variable, kintVariableData $variableData )
  419. {
  420. $variableData->type = 'integer';
  421. $variableData->value = $variable;
  422. }
  423. private static function _parse_null( &$variable, kintVariableData $variableData )
  424. {
  425. $variableData->type = 'NULL';
  426. }
  427. private static function _parse_resource( &$variable, kintVariableData $variableData )
  428. {
  429. $resourceType = get_resource_type( $variable );
  430. $variableData->type = "resource ({$resourceType})";
  431. if ( $resourceType === 'stream' && $meta = stream_get_meta_data( $variable ) ) {
  432. if ( isset( $meta['uri'] ) ) {
  433. $file = $meta['uri'];
  434. if ( function_exists( 'stream_is_local' ) ) {
  435. // Only exists on PHP >= 5.2.4
  436. if ( stream_is_local( $file ) ) {
  437. $file = Kint::shortenPath( $file );
  438. }
  439. }
  440. $variableData->value = $file;
  441. }
  442. }
  443. }
  444. private static function _parse_string( &$variable, kintVariableData $variableData )
  445. {
  446. $variableData->type = 'string';
  447. $encoding = self::_detectEncoding( $variable );
  448. if ( $encoding !== 'ASCII' ) {
  449. $variableData->type .= ' ' . $encoding;
  450. }
  451. $variableData->size = self::_strlen( $variable, $encoding );
  452. if ( Kint::enabled() !== Kint::MODE_RICH ) {
  453. $variableData->value = '"' . self::escape( $variable, $encoding ) . '"';
  454. return;
  455. }
  456. if ( !self::$_placeFullStringInValue ) {
  457. $strippedString = preg_replace( '[\s+]', ' ', $variable );
  458. if ( Kint::$maxStrLength && $variableData->size > Kint::$maxStrLength ) {
  459. // encode and truncate
  460. $variableData->value = '"'
  461. . self::escape( self::_substr( $strippedString, 0, Kint::$maxStrLength, $encoding ), $encoding )
  462. . '&hellip;"';
  463. $variableData->extendedValue = self::escape( $variable, $encoding );
  464. return;
  465. } elseif ( $variable !== $strippedString ) { // omit no data from display
  466. $variableData->value = '"' . self::escape( $variable, $encoding ) . '"';
  467. $variableData->extendedValue = self::escape( $variable, $encoding );
  468. return;
  469. }
  470. }
  471. $variableData->value = '"' . self::escape( $variable, $encoding ) . '"';
  472. }
  473. private static function _parse_unknown( &$variable, kintVariableData $variableData )
  474. {
  475. $type = gettype( $variable );
  476. $variableData->type = "UNKNOWN" . ( !empty( $type ) ? " ({$type})" : '' );
  477. $variableData->value = var_export( $variable, true );
  478. }
  479. }