xref: /PHP-8.2/ext/dom/parentnode.c (revision 043b9e1f)
1 /*
2    +----------------------------------------------------------------------+
3    | PHP Version 7                                                        |
4    +----------------------------------------------------------------------+
5    | Copyright (c) The PHP Group                                          |
6    +----------------------------------------------------------------------+
7    | This source file is subject to version 3.01 of the PHP license,      |
8    | that is bundled with this package in the file LICENSE, and is        |
9    | available through the world-wide-web at the following url:           |
10    | https://www.php.net/license/3_01.txt                                 |
11    | If you did not receive a copy of the PHP license and are unable to   |
12    | obtain it through the world-wide-web, please send a note to          |
13    | license@php.net so we can mail you a copy immediately.               |
14    +----------------------------------------------------------------------+
15    | Authors: Benjamin Eberlei <beberlei@php.net>                         |
16    +----------------------------------------------------------------------+
17 */
18 
19 #ifdef HAVE_CONFIG_H
20 #include "config.h"
21 #endif
22 
23 #include "php.h"
24 #if defined(HAVE_LIBXML) && defined(HAVE_DOM)
25 #include "php_dom.h"
26 
27 /* {{{ firstElementChild DomParentNode
28 readonly=yes
29 URL: https://www.w3.org/TR/dom/#dom-parentnode-firstelementchild
30 */
dom_parent_node_first_element_child_read(dom_object * obj,zval * retval)31 int dom_parent_node_first_element_child_read(dom_object *obj, zval *retval)
32 {
33 	xmlNode *nodep, *first = NULL;
34 
35 	nodep = dom_object_get_node(obj);
36 
37 	if (nodep == NULL) {
38 		php_dom_throw_error(INVALID_STATE_ERR, 1);
39 		return FAILURE;
40 	}
41 
42 	if (dom_node_children_valid(nodep) == SUCCESS) {
43 		first = nodep->children;
44 
45 		while (first && first->type != XML_ELEMENT_NODE) {
46 			first = first->next;
47 		}
48 	}
49 
50 	if (!first) {
51 		ZVAL_NULL(retval);
52 		return SUCCESS;
53 	}
54 
55 	php_dom_create_object(first, retval, obj);
56 	return SUCCESS;
57 }
58 /* }}} */
59 
60 /* {{{ lastElementChild DomParentNode
61 readonly=yes
62 URL: https://www.w3.org/TR/dom/#dom-parentnode-lastelementchild
63 */
dom_parent_node_last_element_child_read(dom_object * obj,zval * retval)64 int dom_parent_node_last_element_child_read(dom_object *obj, zval *retval)
65 {
66 	xmlNode *nodep, *last = NULL;
67 
68 	nodep = dom_object_get_node(obj);
69 
70 	if (nodep == NULL) {
71 		php_dom_throw_error(INVALID_STATE_ERR, 1);
72 		return FAILURE;
73 	}
74 
75 	if (dom_node_children_valid(nodep) == SUCCESS) {
76 		last = nodep->last;
77 
78 		while (last && last->type != XML_ELEMENT_NODE) {
79 			last = last->prev;
80 		}
81 	}
82 
83 	if (!last) {
84 		ZVAL_NULL(retval);
85 		return SUCCESS;
86 	}
87 
88 	php_dom_create_object(last, retval, obj);
89 	return SUCCESS;
90 }
91 /* }}} */
92 
93 /* {{{ childElementCount DomParentNode
94 readonly=yes
95 https://www.w3.org/TR/dom/#dom-parentnode-childelementcount
96 */
dom_parent_node_child_element_count(dom_object * obj,zval * retval)97 int dom_parent_node_child_element_count(dom_object *obj, zval *retval)
98 {
99 	xmlNode *nodep, *first = NULL;
100 	zend_long count = 0;
101 
102 	nodep = dom_object_get_node(obj);
103 
104 	if (nodep == NULL) {
105 		php_dom_throw_error(INVALID_STATE_ERR, 1);
106 		return FAILURE;
107 	}
108 
109 	if (dom_node_children_valid(nodep) == SUCCESS) {
110 		first = nodep->children;
111 
112 		while (first != NULL) {
113 			if (first->type == XML_ELEMENT_NODE) {
114 				count++;
115 			}
116 
117 			first = first->next;
118 		}
119 	}
120 
121 	ZVAL_LONG(retval, count);
122 
123 	return SUCCESS;
124 }
125 /* }}} */
126 
dom_is_node_in_list(const zval * nodes,int nodesc,const xmlNodePtr node_to_find)127 static bool dom_is_node_in_list(const zval *nodes, int nodesc, const xmlNodePtr node_to_find)
128 {
129 	for (int i = 0; i < nodesc; i++) {
130 		if (Z_TYPE(nodes[i]) == IS_OBJECT) {
131 			const zend_class_entry *ce = Z_OBJCE(nodes[i]);
132 
133 			if (instanceof_function(ce, dom_node_class_entry)) {
134 				if (dom_object_get_node(Z_DOMOBJ_P(nodes + i)) == node_to_find) {
135 					return true;
136 				}
137 			}
138 		}
139 	}
140 
141 	return false;
142 }
143 
dom_doc_from_context_node(xmlNodePtr contextNode)144 static xmlDocPtr dom_doc_from_context_node(xmlNodePtr contextNode)
145 {
146 	if (contextNode->type == XML_DOCUMENT_NODE || contextNode->type == XML_HTML_DOCUMENT_NODE) {
147 		return (xmlDocPtr) contextNode;
148 	} else {
149 		return contextNode->doc;
150 	}
151 }
152 
153 /* Citing from the docs (https://gnome.pages.gitlab.gnome.org/libxml2/devhelp/libxml2-tree.html#xmlAddChild):
154  * "Add a new node to @parent, at the end of the child (or property) list merging adjacent TEXT nodes (in which case @cur is freed)".
155  * So we must use a custom way of adding that does not merge. */
dom_add_child_without_merging(xmlNodePtr parent,xmlNodePtr child)156 static void dom_add_child_without_merging(xmlNodePtr parent, xmlNodePtr child)
157 {
158 	if (parent->children == NULL) {
159 		parent->children = child;
160 	} else {
161 		xmlNodePtr last = parent->last;
162 		last->next = child;
163 		child->prev = last;
164 	}
165 	parent->last = child;
166 	child->parent = parent;
167 }
168 
dom_zvals_to_fragment(php_libxml_ref_obj * document,xmlNode * contextNode,zval * nodes,int nodesc)169 xmlNode* dom_zvals_to_fragment(php_libxml_ref_obj *document, xmlNode *contextNode, zval *nodes, int nodesc)
170 {
171 	int i;
172 	xmlDoc *documentNode;
173 	xmlNode *fragment;
174 	xmlNode *newNode;
175 	dom_object *newNodeObj;
176 
177 	documentNode = dom_doc_from_context_node(contextNode);
178 
179 	fragment = xmlNewDocFragment(documentNode);
180 
181 	if (!fragment) {
182 		return NULL;
183 	}
184 
185 	for (i = 0; i < nodesc; i++) {
186 		if (Z_TYPE(nodes[i]) == IS_OBJECT) {
187 			newNodeObj = Z_DOMOBJ_P(&nodes[i]);
188 			newNode = dom_object_get_node(newNodeObj);
189 
190 			if (newNode->parent != NULL) {
191 				xmlUnlinkNode(newNode);
192 			}
193 
194 			newNodeObj->document = document;
195 			xmlSetTreeDoc(newNode, documentNode);
196 
197 			/* Citing from the docs (https://gnome.pages.gitlab.gnome.org/libxml2/devhelp/libxml2-tree.html#xmlAddChild):
198 			 * "Add a new node to @parent, at the end of the child (or property) list merging adjacent TEXT nodes (in which case @cur is freed)".
199 			 * So we must take a copy if this situation arises to prevent a use-after-free. */
200 			bool will_free = newNode->type == XML_TEXT_NODE && fragment->last && fragment->last->type == XML_TEXT_NODE;
201 			if (will_free) {
202 				newNode = xmlCopyNode(newNode, 0);
203 			}
204 
205 			if (newNode->type == XML_DOCUMENT_FRAG_NODE) {
206 				/* Unpack document fragment nodes, the behaviour differs for different libxml2 versions. */
207 				newNode = newNode->children;
208 				while (newNode) {
209 					xmlNodePtr next = newNode->next;
210 					xmlUnlinkNode(newNode);
211 					dom_add_child_without_merging(fragment, newNode);
212 					newNode = next;
213 				}
214 			} else if (!xmlAddChild(fragment, newNode)) {
215 				if (will_free) {
216 					xmlFreeNode(newNode);
217 				}
218 				goto err;
219 			}
220 		} else {
221 			ZEND_ASSERT(Z_TYPE(nodes[i]) == IS_STRING);
222 
223 			newNode = xmlNewDocText(documentNode, (xmlChar *) Z_STRVAL(nodes[i]));
224 
225 			xmlSetTreeDoc(newNode, documentNode);
226 
227 			if (!xmlAddChild(fragment, newNode)) {
228 				xmlFreeNode(newNode);
229 				goto err;
230 			}
231 		}
232 	}
233 
234 	return fragment;
235 
236 err:
237 	xmlFreeNode(fragment);
238 	return NULL;
239 }
240 
dom_fragment_assign_parent_node(xmlNodePtr parentNode,xmlNodePtr fragment)241 static void dom_fragment_assign_parent_node(xmlNodePtr parentNode, xmlNodePtr fragment)
242 {
243 	xmlNodePtr node = fragment->children;
244 
245 	while (node != NULL) {
246 		node->parent = parentNode;
247 
248 		if (node == fragment->last) {
249 			break;
250 		}
251 		node = node->next;
252 	}
253 
254 	fragment->children = NULL;
255 	fragment->last = NULL;
256 }
257 
dom_sanity_check_node_list_for_insertion(php_libxml_ref_obj * document,xmlNodePtr parentNode,zval * nodes,int nodesc)258 static zend_result dom_sanity_check_node_list_for_insertion(php_libxml_ref_obj *document, xmlNodePtr parentNode, zval *nodes, int nodesc)
259 {
260 	if (document == NULL) {
261 		php_dom_throw_error(HIERARCHY_REQUEST_ERR, 1);
262 		return FAILURE;
263 	}
264 
265 	xmlDocPtr documentNode = dom_doc_from_context_node(parentNode);
266 
267 	for (int i = 0; i < nodesc; i++) {
268 		zend_uchar type = Z_TYPE(nodes[i]);
269 		if (type == IS_OBJECT) {
270 			const zend_class_entry *ce = Z_OBJCE(nodes[i]);
271 
272 			if (instanceof_function(ce, dom_node_class_entry)) {
273 				xmlNodePtr node = dom_object_get_node(Z_DOMOBJ_P(nodes + i));
274 
275 				if (!node) {
276 					php_dom_throw_error(INVALID_STATE_ERR, /* strict */ true);
277 					return FAILURE;
278 				}
279 
280 				if (node->doc != documentNode) {
281 					php_dom_throw_error(WRONG_DOCUMENT_ERR, dom_get_strict_error(document));
282 					return FAILURE;
283 				}
284 
285 				if (node->type == XML_ATTRIBUTE_NODE || dom_hierarchy(parentNode, node) != SUCCESS) {
286 					php_dom_throw_error(HIERARCHY_REQUEST_ERR, dom_get_strict_error(document));
287 					return FAILURE;
288 				}
289 			} else {
290 				zend_argument_type_error(i + 1, "must be of type DOMNode|string, %s given", zend_zval_type_name(&nodes[i]));
291 				return FAILURE;
292 			}
293 		} else if (type != IS_STRING) {
294 			zend_argument_type_error(i + 1, "must be of type DOMNode|string, %s given", zend_zval_type_name(&nodes[i]));
295 			return FAILURE;
296 		}
297 	}
298 
299 	return SUCCESS;
300 }
301 
dom_pre_insert(xmlNodePtr insertion_point,xmlNodePtr parentNode,xmlNodePtr newchild,xmlNodePtr fragment)302 static void dom_pre_insert(xmlNodePtr insertion_point, xmlNodePtr parentNode, xmlNodePtr newchild, xmlNodePtr fragment)
303 {
304 	if (!insertion_point) {
305 		/* Place it as last node */
306 		if (parentNode->children) {
307 			/* There are children */
308 			newchild->prev = parentNode->last;
309 			parentNode->last->next = newchild;
310 		} else {
311 			/* No children, because they moved out when they became a fragment */
312 			parentNode->children = newchild;
313 		}
314 		parentNode->last = fragment->last;
315 	} else {
316 		/* Insert fragment before insertion_point */
317 		fragment->last->next = insertion_point;
318 		if (insertion_point->prev) {
319 			insertion_point->prev->next = newchild;
320 			newchild->prev = insertion_point->prev;
321 		}
322 		insertion_point->prev = fragment->last;
323 		if (parentNode->children == insertion_point) {
324 			parentNode->children = newchild;
325 		}
326 	}
327 }
328 
dom_parent_node_append(dom_object * context,zval * nodes,int nodesc)329 void dom_parent_node_append(dom_object *context, zval *nodes, int nodesc)
330 {
331 	xmlNode *parentNode = dom_object_get_node(context);
332 	xmlNodePtr newchild, prevsib;
333 
334 	if (UNEXPECTED(dom_sanity_check_node_list_for_insertion(context->document, parentNode, nodes, nodesc) != SUCCESS)) {
335 		return;
336 	}
337 
338 	xmlNode *fragment = dom_zvals_to_fragment(context->document, parentNode, nodes, nodesc);
339 
340 	if (fragment == NULL) {
341 		return;
342 	}
343 
344 	newchild = fragment->children;
345 	prevsib = parentNode->last;
346 
347 	if (newchild) {
348 		if (prevsib != NULL) {
349 			prevsib->next = newchild;
350 		} else {
351 			parentNode->children = newchild;
352 		}
353 
354 		xmlNodePtr last = fragment->last;
355 		parentNode->last = last;
356 
357 		newchild->prev = prevsib;
358 
359 		dom_fragment_assign_parent_node(parentNode, fragment);
360 
361 		dom_reconcile_ns_list(parentNode->doc, newchild, last);
362 	}
363 
364 	xmlFree(fragment);
365 }
366 
dom_parent_node_prepend(dom_object * context,zval * nodes,int nodesc)367 void dom_parent_node_prepend(dom_object *context, zval *nodes, int nodesc)
368 {
369 	xmlNode *parentNode = dom_object_get_node(context);
370 
371 	if (parentNode->children == NULL) {
372 		dom_parent_node_append(context, nodes, nodesc);
373 		return;
374 	}
375 
376 	if (UNEXPECTED(dom_sanity_check_node_list_for_insertion(context->document, parentNode, nodes, nodesc) != SUCCESS)) {
377 		return;
378 	}
379 
380 	xmlNode *fragment = dom_zvals_to_fragment(context->document, parentNode, nodes, nodesc);
381 
382 	if (fragment == NULL) {
383 		return;
384 	}
385 
386 	xmlNode *newchild = fragment->children;
387 
388 	if (newchild) {
389 		xmlNodePtr last = fragment->last;
390 
391 		dom_pre_insert(parentNode->children, parentNode, newchild, fragment);
392 
393 		dom_fragment_assign_parent_node(parentNode, fragment);
394 
395 		dom_reconcile_ns_list(parentNode->doc, newchild, last);
396 	}
397 
398 	xmlFree(fragment);
399 }
400 
dom_parent_node_after(dom_object * context,zval * nodes,int nodesc)401 void dom_parent_node_after(dom_object *context, zval *nodes, int nodesc)
402 {
403 	/* Spec link: https://dom.spec.whatwg.org/#dom-childnode-after */
404 
405 	xmlNode *prevsib = dom_object_get_node(context);
406 	xmlNodePtr newchild, parentNode;
407 	xmlNode *fragment;
408 	xmlDoc *doc;
409 
410 	/* Spec step 1 */
411 	parentNode = prevsib->parent;
412 	/* Spec step 2 */
413 	if (!parentNode) {
414 		int stricterror = dom_get_strict_error(context->document);
415 		php_dom_throw_error(HIERARCHY_REQUEST_ERR, stricterror);
416 		return;
417 	}
418 
419 	/* Spec step 3: find first following child not in nodes; otherwise null */
420 	xmlNodePtr viable_next_sibling = prevsib->next;
421 	while (viable_next_sibling) {
422 		if (!dom_is_node_in_list(nodes, nodesc, viable_next_sibling)) {
423 			break;
424 		}
425 		viable_next_sibling = viable_next_sibling->next;
426 	}
427 
428 	doc = prevsib->doc;
429 
430 	if (UNEXPECTED(dom_sanity_check_node_list_for_insertion(context->document, parentNode, nodes, nodesc) != SUCCESS)) {
431 		return;
432 	}
433 
434 	/* Spec step 4: convert nodes into fragment */
435 	fragment = dom_zvals_to_fragment(context->document, parentNode, nodes, nodesc);
436 
437 	if (fragment == NULL) {
438 		return;
439 	}
440 
441 	newchild = fragment->children;
442 
443 	if (newchild) {
444 		xmlNodePtr last = fragment->last;
445 
446 		/* Step 5: place fragment into the parent before viable_next_sibling */
447 		dom_pre_insert(viable_next_sibling, parentNode, newchild, fragment);
448 
449 		dom_fragment_assign_parent_node(parentNode, fragment);
450 		dom_reconcile_ns_list(doc, newchild, last);
451 	}
452 
453 	xmlFree(fragment);
454 }
455 
dom_parent_node_before(dom_object * context,zval * nodes,int nodesc)456 void dom_parent_node_before(dom_object *context, zval *nodes, int nodesc)
457 {
458 	/* Spec link: https://dom.spec.whatwg.org/#dom-childnode-before */
459 
460 	xmlNode *nextsib = dom_object_get_node(context);
461 	xmlNodePtr newchild, parentNode;
462 	xmlNode *fragment;
463 	xmlDoc *doc;
464 
465 	/* Spec step 1 */
466 	parentNode = nextsib->parent;
467 	/* Spec step 2 */
468 	if (!parentNode) {
469 		int stricterror = dom_get_strict_error(context->document);
470 		php_dom_throw_error(HIERARCHY_REQUEST_ERR, stricterror);
471 		return;
472 	}
473 
474 	/* Spec step 3: find first following child not in nodes; otherwise null */
475 	xmlNodePtr viable_previous_sibling = nextsib->prev;
476 	while (viable_previous_sibling) {
477 		if (!dom_is_node_in_list(nodes, nodesc, viable_previous_sibling)) {
478 			break;
479 		}
480 		viable_previous_sibling = viable_previous_sibling->prev;
481 	}
482 
483 	doc = nextsib->doc;
484 
485 	if (UNEXPECTED(dom_sanity_check_node_list_for_insertion(context->document, parentNode, nodes, nodesc) != SUCCESS)) {
486 		return;
487 	}
488 
489 	/* Spec step 4: convert nodes into fragment */
490 	fragment = dom_zvals_to_fragment(context->document, parentNode, nodes, nodesc);
491 
492 	if (fragment == NULL) {
493 		return;
494 	}
495 
496 	newchild = fragment->children;
497 
498 	if (newchild) {
499 		xmlNodePtr last = fragment->last;
500 
501 		/* Step 5: if viable_previous_sibling is null, set it to the parent's first child, otherwise viable_previous_sibling's next sibling */
502 		if (!viable_previous_sibling) {
503 			viable_previous_sibling = parentNode->children;
504 		} else {
505 			viable_previous_sibling = viable_previous_sibling->next;
506 		}
507 		/* Step 6: place fragment into the parent after viable_previous_sibling */
508 		dom_pre_insert(viable_previous_sibling, parentNode, newchild, fragment);
509 
510 		dom_fragment_assign_parent_node(parentNode, fragment);
511 		dom_reconcile_ns_list(doc, newchild, last);
512 	}
513 
514 	xmlFree(fragment);
515 }
516 
dom_child_removal_preconditions(const xmlNodePtr child,int stricterror)517 static zend_result dom_child_removal_preconditions(const xmlNodePtr child, int stricterror)
518 {
519 	if (dom_node_is_read_only(child) == SUCCESS ||
520 		(child->parent != NULL && dom_node_is_read_only(child->parent) == SUCCESS)) {
521 		php_dom_throw_error(NO_MODIFICATION_ALLOWED_ERR, stricterror);
522 		return FAILURE;
523 	}
524 
525 	if (!child->parent) {
526 		php_dom_throw_error(NOT_FOUND_ERR, stricterror);
527 		return FAILURE;
528 	}
529 
530 	if (dom_node_children_valid(child->parent) == FAILURE) {
531 		return FAILURE;
532 	}
533 
534 	xmlNodePtr children = child->parent->children;
535 	if (!children) {
536 		php_dom_throw_error(NOT_FOUND_ERR, stricterror);
537 		return FAILURE;
538 	}
539 
540 	return SUCCESS;
541 }
542 
dom_child_node_remove(dom_object * context)543 void dom_child_node_remove(dom_object *context)
544 {
545 	xmlNode *child = dom_object_get_node(context);
546 	xmlNodePtr children;
547 	int stricterror;
548 
549 	stricterror = dom_get_strict_error(context->document);
550 
551 	if (UNEXPECTED(dom_child_removal_preconditions(child, stricterror) != SUCCESS)) {
552 		return;
553 	}
554 
555 	children = child->parent->children;
556 	while (children) {
557 		if (children == child) {
558 			xmlUnlinkNode(child);
559 			return;
560 		}
561 		children = children->next;
562 	}
563 
564 	php_dom_throw_error(NOT_FOUND_ERR, stricterror);
565 }
566 
dom_child_replace_with(dom_object * context,zval * nodes,int nodesc)567 void dom_child_replace_with(dom_object *context, zval *nodes, int nodesc)
568 {
569 	/* Spec link: https://dom.spec.whatwg.org/#dom-childnode-replacewith */
570 
571 	xmlNodePtr child = dom_object_get_node(context);
572 
573 	/* Spec step 1 */
574 	xmlNodePtr parentNode = child->parent;
575 	/* Spec step 2 */
576 	if (!parentNode) {
577 		int stricterror = dom_get_strict_error(context->document);
578 		php_dom_throw_error(HIERARCHY_REQUEST_ERR, stricterror);
579 		return;
580 	}
581 
582 	int stricterror = dom_get_strict_error(context->document);
583 	if (UNEXPECTED(dom_child_removal_preconditions(child, stricterror) != SUCCESS)) {
584 		return;
585 	}
586 
587 	/* Spec step 3: find first following child not in nodes; otherwise null */
588 	xmlNodePtr viable_next_sibling = child->next;
589 	while (viable_next_sibling) {
590 		if (!dom_is_node_in_list(nodes, nodesc, viable_next_sibling)) {
591 			break;
592 		}
593 		viable_next_sibling = viable_next_sibling->next;
594 	}
595 
596 	if (UNEXPECTED(dom_sanity_check_node_list_for_insertion(context->document, parentNode, nodes, nodesc) != SUCCESS)) {
597 		return;
598 	}
599 
600 	/* Spec step 4: convert nodes into fragment */
601 	xmlNodePtr fragment = dom_zvals_to_fragment(context->document, parentNode, nodes, nodesc);
602 	if (UNEXPECTED(fragment == NULL)) {
603 		return;
604 	}
605 
606 	/* Spec step 5: perform the replacement */
607 
608 	xmlNodePtr newchild = fragment->children;
609 	xmlDocPtr doc = parentNode->doc;
610 
611 	/* Unlink and free it unless it became a part of the fragment. */
612 	if (child->parent != fragment) {
613 		xmlUnlinkNode(child);
614 	}
615 
616 	if (newchild) {
617 		xmlNodePtr last = fragment->last;
618 
619 		dom_pre_insert(viable_next_sibling, parentNode, newchild, fragment);
620 
621 		dom_fragment_assign_parent_node(parentNode, fragment);
622 		dom_reconcile_ns_list(doc, newchild, last);
623 	}
624 
625 	xmlFree(fragment);
626 }
627 
628 #endif
629