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 DOMNamedNodeMap;
29: use DOMNode;
30: use DOMNodeList;
31: use Kint\Object\BasicObject;
32: use Kint\Object\InstanceObject;
33: use Kint\Object\Representation\Representation;
34:
35: /**
36: * The DOMDocument parser plugin is particularly useful as it is both the only
37: * way to see inside the DOMNode without print_r, and the only way to see mixed
38: * text and node inside XML (SimpleXMLElement will strip out the text).
39: */
40: class DOMDocumentPlugin extends Plugin
41: {
42: /**
43: * List of properties to skip parsing.
44: *
45: * The properties of a DOMNode can do a *lot* of damage to debuggers. The
46: * DOMNode contains not one, not two, not three, not four, not 5, not 6,
47: * not 7 but 8 different ways to recurse into itself:
48: * * firstChild
49: * * lastChild
50: * * previousSibling
51: * * nextSibling
52: * * ownerDocument
53: * * parentNode
54: * * childNodes
55: * * attributes
56: *
57: * All of this combined: the tiny SVGs used as the caret in Kint are already
58: * enough to make parsing and rendering take over a second, and send memory
59: * usage over 128 megs. So we blacklist every field we don't strictly need
60: * and hope that that's good enough.
61: *
62: * In retrospect - this is probably why print_r does the same
63: *
64: * @var array
65: */
66: public static $blacklist = array(
67: 'parentNode' => 'DOMNode',
68: 'firstChild' => 'DOMNode',
69: 'lastChild' => 'DOMNode',
70: 'previousSibling' => 'DOMNode',
71: 'nextSibling' => 'DOMNode',
72: 'ownerDocument' => 'DOMDocument',
73: );
74:
75: /**
76: * Show all properties and methods.
77: *
78: * @var bool
79: */
80: public static $verbose = false;
81:
82: public function getTypes()
83: {
84: return array('object');
85: }
86:
87: public function getTriggers()
88: {
89: return Parser::TRIGGER_SUCCESS;
90: }
91:
92: public function parse(&$var, BasicObject &$o, $trigger)
93: {
94: if (!$o instanceof InstanceObject) {
95: return;
96: }
97:
98: if ($var instanceof DOMNamedNodeMap || $var instanceof DOMNodeList) {
99: return $this->parseList($var, $o, $trigger);
100: }
101:
102: if ($var instanceof DOMNode) {
103: return $this->parseNode($var, $o);
104: }
105: }
106:
107: protected function parseList(&$var, InstanceObject &$o, $trigger)
108: {
109: // Recursion should never happen, should always be stopped at the parent
110: // DOMNode. Depth limit on the other hand we're going to skip since
111: // that would show an empty iterator and rather useless. Let the depth
112: // limit hit the children (DOMNodeList only has DOMNode as children)
113: if ($trigger & Parser::TRIGGER_RECURSION) {
114: return;
115: }
116:
117: $o->size = $var->length;
118: if (0 === $o->size) {
119: $o->replaceRepresentation(new Representation('Iterator'));
120: $o->size = null;
121:
122: return;
123: }
124:
125: // Depth limit
126: // Make empty iterator representation since we need it in DOMNode to point out depth limits
127: if ($this->parser->getDepthLimit() && $o->depth + 1 >= $this->parser->getDepthLimit()) {
128: $b = new BasicObject();
129: $b->name = $o->classname.' Iterator Contents';
130: $b->access_path = 'iterator_to_array('.$o->access_path.')';
131: $b->depth = $o->depth + 1;
132: $b->hints[] = 'depth_limit';
133:
134: $r = new Representation('Iterator');
135: $r->contents = array($b);
136: $o->replaceRepresentation($r, 0);
137:
138: return;
139: }
140:
141: $data = \iterator_to_array($var);
142:
143: $r = new Representation('Iterator');
144: $o->replaceRepresentation($r, 0);
145:
146: foreach ($data as $key => $item) {
147: $base_obj = new BasicObject();
148: $base_obj->depth = $o->depth + 1;
149: $base_obj->name = $item->nodeName;
150:
151: if ($o->access_path) {
152: if ($var instanceof DOMNamedNodeMap) {
153: $base_obj->access_path = $o->access_path.'->getNamedItem('.\var_export($key, true).')';
154: } elseif ($var instanceof DOMNodeList) {
155: $base_obj->access_path = $o->access_path.'->item('.\var_export($key, true).')';
156: } else {
157: $base_obj->access_path = 'iterator_to_array('.$o->access_path.')';
158: }
159: }
160:
161: $r->contents[] = $this->parser->parse($item, $base_obj);
162: }
163: }
164:
165: protected function parseNode(&$var, InstanceObject &$o)
166: {
167: // Fill the properties
168: // They can't be enumerated through reflection or casting,
169: // so we have to trust the docs and try them one at a time
170: $known_properties = array(
171: 'nodeValue',
172: 'childNodes',
173: 'attributes',
174: );
175:
176: if (self::$verbose) {
177: $known_properties = array(
178: 'nodeName',
179: 'nodeValue',
180: 'nodeType',
181: 'parentNode',
182: 'childNodes',
183: 'firstChild',
184: 'lastChild',
185: 'previousSibling',
186: 'nextSibling',
187: 'attributes',
188: 'ownerDocument',
189: 'namespaceURI',
190: 'prefix',
191: 'localName',
192: 'baseURI',
193: 'textContent',
194: );
195: }
196:
197: $childNodes = array();
198: $attributes = array();
199:
200: $rep = $o->value;
201:
202: foreach ($known_properties as $prop) {
203: $prop_obj = $this->parseProperty($o, $prop, $var);
204: $rep->contents[] = $prop_obj;
205:
206: if ('childNodes' === $prop) {
207: $childNodes = $prop_obj->getRepresentation('iterator');
208: } elseif ('attributes' === $prop) {
209: $attributes = $prop_obj->getRepresentation('iterator');
210: }
211: }
212:
213: if (!self::$verbose) {
214: $o->removeRepresentation('methods');
215: $o->removeRepresentation('properties');
216: }
217:
218: // Attributes and comments and text nodes don't
219: // need children or attributes of their own
220: if (\in_array($o->classname, array('DOMAttr', 'DOMText', 'DOMComment'), true)) {
221: return;
222: }
223:
224: // Set the attributes
225: if ($attributes) {
226: $a = new Representation('Attributes');
227: foreach ($attributes->contents as $attribute) {
228: $a->contents[] = self::textualNodeToString($attribute);
229: }
230: $o->addRepresentation($a, 0);
231: }
232:
233: // Set the children
234: if ($childNodes) {
235: $c = new Representation('Children');
236:
237: if (1 === \count($childNodes->contents) && ($node = \reset($childNodes->contents)) && \in_array('depth_limit', $node->hints, true)) {
238: $n = new InstanceObject();
239: $n->transplant($node);
240: $n->name = 'childNodes';
241: $n->classname = 'DOMNodeList';
242: $c->contents = array($n);
243: } else {
244: foreach ($childNodes->contents as $index => $node) {
245: // Shortcircuit text nodes to plain strings
246: if ('DOMText' === $node->classname || 'DOMComment' === $node->classname) {
247: $node = self::textualNodeToString($node);
248:
249: // And remove them if they're empty
250: if (\ctype_space($node->value->contents) || '' === $node->value->contents) {
251: continue;
252: }
253: }
254:
255: $c->contents[] = $node;
256: }
257: }
258:
259: $o->addRepresentation($c, 0);
260: }
261:
262: if (isset($c) && \count($c->contents)) {
263: $o->size = \count($c->contents);
264: }
265:
266: if (!$o->size) {
267: $o->size = null;
268: }
269: }
270:
271: protected function parseProperty(InstanceObject $o, $prop, &$var)
272: {
273: // Duplicating (And slightly optimizing) the Parser::parseObject() code here
274: $base_obj = new BasicObject();
275: $base_obj->depth = $o->depth + 1;
276: $base_obj->owner_class = $o->classname;
277: $base_obj->name = $prop;
278: $base_obj->operator = BasicObject::OPERATOR_OBJECT;
279: $base_obj->access = BasicObject::ACCESS_PUBLIC;
280:
281: if (null !== $o->access_path) {
282: $base_obj->access_path = $o->access_path;
283:
284: if (\preg_match('/^[A-Za-z0-9_]+$/', $base_obj->name)) {
285: $base_obj->access_path .= '->'.$base_obj->name;
286: } else {
287: $base_obj->access_path .= '->{'.\var_export($base_obj->name, true).'}';
288: }
289: }
290:
291: if (!isset($var->{$prop})) {
292: $base_obj->type = 'null';
293: } elseif (isset(self::$blacklist[$prop])) {
294: $b = new InstanceObject();
295: $b->transplant($base_obj);
296: $base_obj = $b;
297:
298: $base_obj->hints[] = 'blacklist';
299: $base_obj->classname = self::$blacklist[$prop];
300: } elseif ('attributes' === $prop) {
301: $base_obj = $this->parser->parseDeep($var->{$prop}, $base_obj);
302: } else {
303: $base_obj = $this->parser->parse($var->{$prop}, $base_obj);
304: }
305:
306: return $base_obj;
307: }
308:
309: protected static function textualNodeToString(InstanceObject $o)
310: {
311: if (empty($o->value) || empty($o->value->contents) || empty($o->classname)) {
312: return;
313: }
314:
315: if (!\in_array($o->classname, array('DOMText', 'DOMAttr', 'DOMComment'), true)) {
316: return;
317: }
318:
319: foreach ($o->value->contents as $property) {
320: if ('nodeValue' === $property->name) {
321: $ret = clone $property;
322: $ret->name = $o->name;
323:
324: return $ret;
325: }
326: }
327: }
328: }
329: