1: <?php
2:
3: /*
4: * The MIT License (MIT)
5: *
6: * Copyright (c) 2013 Jonathan Vollebregt (jnvsor@gmail.com), Rokas Šleinius (raveren@gmail.com)
7: *
8: * Permission is hereby granted, free of charge, to any person obtaining a copy of
9: * this software and associated documentation files (the "Software"), to deal in
10: * the Software without restriction, including without limitation the rights to
11: * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
12: * the Software, and to permit persons to whom the Software is furnished to do so,
13: * subject to the following conditions:
14: *
15: * The above copyright notice and this permission notice shall be included in all
16: * copies or substantial portions of the Software.
17: *
18: * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19: * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
20: * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
21: * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
22: * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
23: * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
24: */
25:
26: namespace Kint\Parser;
27:
28: use DomainException;
29: use Exception;
30: use Kint\Object\BasicObject;
31: use Kint\Object\BlobObject;
32: use Kint\Object\InstanceObject;
33: use Kint\Object\Representation\Representation;
34: use Kint\Object\ResourceObject;
35: use ReflectionObject;
36: use stdClass;
37:
38: class Parser
39: {
40: /**
41: * Plugin triggers.
42: *
43: * These are constants indicating trigger points for plugins
44: *
45: * BEGIN: Before normal parsing
46: * SUCCESS: After successful parsing
47: * RECURSION: After parsing cancelled by recursion
48: * DEPTH_LIMIT: After parsing cancelled by depth limit
49: * COMPLETE: SUCCESS | RECURSION | DEPTH_LIMIT
50: *
51: * While a plugin's getTriggers may return any of these
52: */
53: const TRIGGER_NONE = 0;
54: const TRIGGER_BEGIN = 1;
55: const TRIGGER_SUCCESS = 2;
56: const TRIGGER_RECURSION = 4;
57: const TRIGGER_DEPTH_LIMIT = 8;
58: const TRIGGER_COMPLETE = 14;
59:
60: protected $caller_class;
61: protected $depth_limit = false;
62: protected $marker;
63: protected $object_hashes = array();
64: protected $parse_break = false;
65: protected $plugins = array();
66:
67: /**
68: * @param false|int $depth_limit Maximum depth to parse data
69: * @param null|string $caller Caller class name
70: */
71: public function __construct($depth_limit = false, $caller = null)
72: {
73: $this->marker = \uniqid("kint\0", true);
74:
75: $this->caller_class = $caller;
76:
77: if ($depth_limit) {
78: $this->depth_limit = $depth_limit;
79: }
80: }
81:
82: /**
83: * Set the caller class.
84: *
85: * @param null|string $caller Caller class name
86: */
87: public function setCallerClass($caller = null)
88: {
89: $this->noRecurseCall();
90:
91: $this->caller_class = $caller;
92: }
93:
94: public function getCallerClass()
95: {
96: return $this->caller_class;
97: }
98:
99: /**
100: * Set the depth limit.
101: *
102: * @param false|int $depth_limit Maximum depth to parse data
103: */
104: public function setDepthLimit($depth_limit = false)
105: {
106: $this->noRecurseCall();
107:
108: $this->depth_limit = $depth_limit;
109: }
110:
111: public function getDepthLimit()
112: {
113: return $this->depth_limit;
114: }
115:
116: /**
117: * Disables the depth limit and parses a variable.
118: *
119: * This should not be used unless you know what you're doing!
120: *
121: * @param mixed $var The input variable
122: * @param BasicObject $o The base object
123: *
124: * @return BasicObject
125: */
126: public function parseDeep(&$var, BasicObject $o)
127: {
128: $depth_limit = $this->depth_limit;
129: $this->depth_limit = false;
130:
131: $out = $this->parse($var, $o);
132:
133: $this->depth_limit = $depth_limit;
134:
135: return $out;
136: }
137:
138: /**
139: * Parses a variable into a Kint object structure.
140: *
141: * @param mixed $var The input variable
142: * @param BasicObject $o The base object
143: *
144: * @return BasicObject
145: */
146: public function parse(&$var, BasicObject $o)
147: {
148: $o->type = \strtolower(\gettype($var));
149:
150: if (!$this->applyPlugins($var, $o, self::TRIGGER_BEGIN)) {
151: return $o;
152: }
153:
154: switch ($o->type) {
155: case 'array':
156: return $this->parseArray($var, $o);
157: case 'boolean':
158: case 'double':
159: case 'integer':
160: case 'null':
161: return $this->parseGeneric($var, $o);
162: case 'object':
163: return $this->parseObject($var, $o);
164: case 'resource':
165: return $this->parseResource($var, $o);
166: case 'string':
167: return $this->parseString($var, $o);
168: default:
169: return $this->parseUnknown($var, $o);
170: }
171: }
172:
173: public function addPlugin(Plugin $p)
174: {
175: if (!$types = $p->getTypes()) {
176: return false;
177: }
178:
179: if (!$triggers = $p->getTriggers()) {
180: return false;
181: }
182:
183: $p->setParser($this);
184:
185: foreach ($types as $type) {
186: if (!isset($this->plugins[$type])) {
187: $this->plugins[$type] = array(
188: self::TRIGGER_BEGIN => array(),
189: self::TRIGGER_SUCCESS => array(),
190: self::TRIGGER_RECURSION => array(),
191: self::TRIGGER_DEPTH_LIMIT => array(),
192: );
193: }
194:
195: foreach ($this->plugins[$type] as $trigger => &$pool) {
196: if ($triggers & $trigger) {
197: $pool[] = $p;
198: }
199: }
200: }
201:
202: return true;
203: }
204:
205: public function clearPlugins()
206: {
207: $this->plugins = array();
208: }
209:
210: public function haltParse()
211: {
212: $this->parse_break = true;
213: }
214:
215: public function childHasPath(InstanceObject $parent, BasicObject $child)
216: {
217: if ('object' === $parent->type && (null !== $parent->access_path || $child->static || $child->const)) {
218: if (BasicObject::ACCESS_PUBLIC === $child->access) {
219: return true;
220: }
221:
222: if (BasicObject::ACCESS_PRIVATE === $child->access && $this->caller_class) {
223: if ($this->caller_class === $child->owner_class) {
224: return true;
225: }
226: } elseif (BasicObject::ACCESS_PROTECTED === $child->access && $this->caller_class) {
227: if ($this->caller_class === $child->owner_class) {
228: return true;
229: }
230:
231: if (\is_subclass_of($this->caller_class, $child->owner_class)) {
232: return true;
233: }
234:
235: if (\is_subclass_of($child->owner_class, $this->caller_class)) {
236: return true;
237: }
238: }
239: }
240:
241: return false;
242: }
243:
244: /**
245: * Returns an array without the recursion marker in it.
246: *
247: * DO NOT pass an array that has had it's marker removed back
248: * into the parser, it will result in an extra recursion
249: *
250: * @param array $array Array potentially containing a recursion marker
251: *
252: * @return array Array with recursion marker removed
253: */
254: public function getCleanArray(array $array)
255: {
256: unset($array[$this->marker]);
257:
258: return $array;
259: }
260:
261: protected function noRecurseCall()
262: {
263: $bt = \debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT | DEBUG_BACKTRACE_IGNORE_ARGS);
264:
265: $caller_frame = array(
266: 'function' => __FUNCTION__,
267: );
268:
269: while (isset($bt[0]['object']) && $bt[0]['object'] === $this) {
270: $caller_frame = \array_shift($bt);
271: }
272:
273: foreach ($bt as $frame) {
274: if (isset($frame['object']) && $frame['object'] === $this) {
275: throw new DomainException(__CLASS__.'::'.$caller_frame['function'].' cannot be called from inside a parse');
276: }
277: }
278: }
279:
280: private function parseGeneric(&$var, BasicObject $o)
281: {
282: $rep = new Representation('Contents');
283: $rep->contents = $var;
284: $rep->implicit_label = true;
285: $o->addRepresentation($rep);
286: $o->value = $rep;
287:
288: $this->applyPlugins($var, $o, self::TRIGGER_SUCCESS);
289:
290: return $o;
291: }
292:
293: /**
294: * Parses a string into a Kint BlobObject structure.
295: *
296: * @param string $var The input variable
297: * @param BasicObject $o The base object
298: *
299: * @return BasicObject
300: */
301: private function parseString(&$var, BasicObject $o)
302: {
303: $string = new BlobObject();
304: $string->transplant($o);
305: $string->encoding = BlobObject::detectEncoding($var);
306: $string->size = BlobObject::strlen($var, $string->encoding);
307:
308: $rep = new Representation('Contents');
309: $rep->contents = $var;
310: $rep->implicit_label = true;
311:
312: $string->addRepresentation($rep);
313: $string->value = $rep;
314:
315: $this->applyPlugins($var, $string, self::TRIGGER_SUCCESS);
316:
317: return $string;
318: }
319:
320: /**
321: * Parses an array into a Kint object structure.
322: *
323: * @param array $var The input variable
324: * @param BasicObject $o The base object
325: *
326: * @return BasicObject
327: */
328: private function parseArray(array &$var, BasicObject $o)
329: {
330: $array = new BasicObject();
331: $array->transplant($o);
332: $array->size = \count($var);
333:
334: if (isset($var[$this->marker])) {
335: --$array->size;
336: $array->hints[] = 'recursion';
337:
338: $this->applyPlugins($var, $array, self::TRIGGER_RECURSION);
339:
340: return $array;
341: }
342:
343: $rep = new Representation('Contents');
344: $rep->implicit_label = true;
345: $array->addRepresentation($rep);
346: $array->value = $rep;
347:
348: if (!$array->size) {
349: $this->applyPlugins($var, $array, self::TRIGGER_SUCCESS);
350:
351: return $array;
352: }
353:
354: if ($this->depth_limit && $o->depth >= $this->depth_limit) {
355: $array->hints[] = 'depth_limit';
356:
357: $this->applyPlugins($var, $array, self::TRIGGER_DEPTH_LIMIT);
358:
359: return $array;
360: }
361:
362: $copy = \array_values($var);
363:
364: // It's really really hard to access numeric string keys in arrays,
365: // and it's really really hard to access integer properties in
366: // objects, so we just use array_values and index by counter to get
367: // at it reliably for reference testing. This also affects access
368: // paths since it's pretty much impossible to access these things
369: // without complicated stuff you should never need to do.
370: $i = 0;
371:
372: // Set the marker for recursion
373: $var[$this->marker] = $array->depth;
374:
375: $refmarker = new stdClass();
376:
377: foreach ($var as $key => &$val) {
378: if ($key === $this->marker) {
379: continue;
380: }
381:
382: $child = new BasicObject();
383: $child->name = $key;
384: $child->depth = $array->depth + 1;
385: $child->access = BasicObject::ACCESS_NONE;
386: $child->operator = BasicObject::OPERATOR_ARRAY;
387:
388: if (null !== $array->access_path) {
389: if (\is_string($key) && (string) (int) $key === $key) {
390: $child->access_path = 'array_values('.$array->access_path.')['.$i.']'; // @codeCoverageIgnore
391: } else {
392: $child->access_path = $array->access_path.'['.\var_export($key, true).']';
393: }
394: }
395:
396: $stash = $val;
397: $copy[$i] = $refmarker;
398: if ($val === $refmarker) {
399: $child->reference = true;
400: $val = $stash;
401: }
402:
403: $rep->contents[] = $this->parse($val, $child);
404: ++$i;
405: }
406:
407: $this->applyPlugins($var, $array, self::TRIGGER_SUCCESS);
408: unset($var[$this->marker]);
409:
410: return $array;
411: }
412:
413: /**
414: * Parses an object into a Kint InstanceObject structure.
415: *
416: * @param object $var The input variable
417: * @param BasicObject $o The base object
418: *
419: * @return BasicObject
420: */
421: private function parseObject(&$var, BasicObject $o)
422: {
423: $hash = \spl_object_hash($var);
424: $values = (array) $var;
425:
426: $object = new InstanceObject();
427: $object->transplant($o);
428: $object->classname = \get_class($var);
429: $object->hash = $hash;
430: $object->size = \count($values);
431:
432: if (isset($this->object_hashes[$hash])) {
433: $object->hints[] = 'recursion';
434:
435: $this->applyPlugins($var, $object, self::TRIGGER_RECURSION);
436:
437: return $object;
438: }
439:
440: $this->object_hashes[$hash] = $object;
441:
442: if ($this->depth_limit && $o->depth >= $this->depth_limit) {
443: $object->hints[] = 'depth_limit';
444:
445: $this->applyPlugins($var, $object, self::TRIGGER_DEPTH_LIMIT);
446: unset($this->object_hashes[$hash]);
447:
448: return $object;
449: }
450:
451: $reflector = new ReflectionObject($var);
452:
453: if ($reflector->isUserDefined()) {
454: $object->filename = $reflector->getFileName();
455: $object->startline = $reflector->getStartLine();
456: }
457:
458: $rep = new Representation('Properties');
459:
460: $copy = \array_values($values);
461: $refmarker = new stdClass();
462: $i = 0;
463:
464: // Reflection will not show parent classes private properties, and if a
465: // property was unset it will happly trigger a notice looking for it.
466: foreach ($values as $key => &$val) {
467: // Casting object to array:
468: // private properties show in the form "\0$owner_class_name\0$property_name";
469: // protected properties show in the form "\0*\0$property_name";
470: // public properties show in the form "$property_name";
471: // http://www.php.net/manual/en/language.types.array.php#language.types.array.casting
472:
473: $child = new BasicObject();
474: $child->depth = $object->depth + 1;
475: $child->owner_class = $object->classname;
476: $child->operator = BasicObject::OPERATOR_OBJECT;
477: $child->access = BasicObject::ACCESS_PUBLIC;
478:
479: $split_key = \explode("\0", $key, 3);
480:
481: if (3 === \count($split_key) && '' === $split_key[0]) {
482: $child->name = $split_key[2];
483: if ('*' === $split_key[1]) {
484: $child->access = BasicObject::ACCESS_PROTECTED;
485: } else {
486: $child->access = BasicObject::ACCESS_PRIVATE;
487: $child->owner_class = $split_key[1];
488: }
489: } elseif (KINT_PHP72) {
490: $child->name = (string) $key;
491: } else {
492: $child->name = $key; // @codeCoverageIgnore
493: }
494:
495: if ($this->childHasPath($object, $child)) {
496: $child->access_path = $object->access_path;
497:
498: if (!KINT_PHP72 && \is_int($child->name)) {
499: $child->access_path = 'array_values((array) '.$child->access_path.')['.$i.']'; // @codeCoverageIgnore
500: } elseif (\preg_match('/^[a-zA-Z_\\x7f-\\xff][a-zA-Z0-9_\\x7f-\\xff]*$/', $child->name)) {
501: $child->access_path .= '->'.$child->name;
502: } else {
503: $child->access_path .= '->{'.\var_export((string) $child->name, true).'}';
504: }
505: }
506:
507: $stash = $val;
508: $copy[$i] = $refmarker;
509: if ($val === $refmarker) {
510: $child->reference = true;
511: $val = $stash;
512: }
513:
514: $rep->contents[] = $this->parse($val, $child);
515: ++$i;
516: }
517:
518: $object->addRepresentation($rep);
519: $object->value = $rep;
520: $this->applyPlugins($var, $object, self::TRIGGER_SUCCESS);
521: unset($this->object_hashes[$hash]);
522:
523: return $object;
524: }
525:
526: /**
527: * Parses a resource into a Kint ResourceObject structure.
528: *
529: * @param resource $var The input variable
530: * @param BasicObject $o The base object
531: *
532: * @return BasicObject
533: */
534: private function parseResource(&$var, BasicObject $o)
535: {
536: $resource = new ResourceObject();
537: $resource->transplant($o);
538: $resource->resource_type = \get_resource_type($var);
539:
540: $this->applyPlugins($var, $resource, self::TRIGGER_SUCCESS);
541:
542: return $resource;
543: }
544:
545: /**
546: * Parses an unknown into a Kint object structure.
547: *
548: * @param mixed $var The input variable
549: * @param BasicObject $o The base object
550: *
551: * @return BasicObject
552: */
553: private function parseUnknown(&$var, BasicObject $o)
554: {
555: $o->type = 'unknown';
556: $this->applyPlugins($var, $o, self::TRIGGER_SUCCESS);
557:
558: return $o;
559: }
560:
561: /**
562: * Applies plugins for an object type.
563: *
564: * @param mixed $var variable
565: * @param BasicObject $o Kint object parsed so far
566: * @param int $trigger The trigger to check for the plugins
567: *
568: * @return bool Continue parsing
569: */
570: private function applyPlugins(&$var, BasicObject &$o, $trigger)
571: {
572: $break_stash = $this->parse_break;
573:
574: /** @var bool Psalm bug workaround */
575: $this->parse_break = false;
576:
577: $plugins = array();
578:
579: if (isset($this->plugins[$o->type][$trigger])) {
580: $plugins = $this->plugins[$o->type][$trigger];
581: }
582:
583: foreach ($plugins as $plugin) {
584: try {
585: $plugin->parse($var, $o, $trigger);
586: } catch (Exception $e) {
587: \trigger_error(
588: 'An exception ('.\get_class($e).') was thrown in '.$e->getFile().' on line '.$e->getLine().' while executing Kint Parser Plugin "'.\get_class($plugin).'". Error message: '.$e->getMessage(),
589: E_USER_WARNING
590: );
591: }
592:
593: if ($this->parse_break) {
594: $this->parse_break = $break_stash;
595:
596: return false;
597: }
598: }
599:
600: $this->parse_break = $break_stash;
601:
602: return true;
603: }
604: }
605: