xref: /PHP-8.1/ext/dom/parentnode.c (revision d19e4da1)
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 
dom_zvals_to_fragment(php_libxml_ref_obj * document,xmlNode * contextNode,zval * nodes,int nodesc)153 xmlNode* dom_zvals_to_fragment(php_libxml_ref_obj *document, xmlNode *contextNode, zval *nodes, int nodesc)
154 {
155 	int i;
156 	xmlDoc *documentNode;
157 	xmlNode *fragment;
158 	xmlNode *newNode;
159 	dom_object *newNodeObj;
160 
161 	documentNode = dom_doc_from_context_node(contextNode);
162 
163 	fragment = xmlNewDocFragment(documentNode);
164 
165 	if (!fragment) {
166 		return NULL;
167 	}
168 
169 	for (i = 0; i < nodesc; i++) {
170 		if (Z_TYPE(nodes[i]) == IS_OBJECT) {
171 			newNodeObj = Z_DOMOBJ_P(&nodes[i]);
172 			newNode = dom_object_get_node(newNodeObj);
173 
174 			if (newNode->parent != NULL) {
175 				xmlUnlinkNode(newNode);
176 			}
177 
178 			newNodeObj->document = document;
179 			xmlSetTreeDoc(newNode, documentNode);
180 
181 			/* Citing from the docs (https://gnome.pages.gitlab.gnome.org/libxml2/devhelp/libxml2-tree.html#xmlAddChild):
182 			 * "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)".
183 			 * So we must take a copy if this situation arises to prevent a use-after-free. */
184 			bool will_free = newNode->type == XML_TEXT_NODE && fragment->last && fragment->last->type == XML_TEXT_NODE;
185 			if (will_free) {
186 				newNode = xmlCopyNode(newNode, 1);
187 			}
188 
189 			if (newNode->type == XML_DOCUMENT_FRAG_NODE) {
190 				/* Unpack document fragment nodes, the behaviour differs for different libxml2 versions. */
191 				newNode = newNode->children;
192 				while (newNode) {
193 					xmlNodePtr next = newNode->next;
194 					xmlUnlinkNode(newNode);
195 					if (!xmlAddChild(fragment, newNode)) {
196 						goto err;
197 					}
198 					newNode = next;
199 				}
200 			} else if (!xmlAddChild(fragment, newNode)) {
201 				if (will_free) {
202 					xmlFreeNode(newNode);
203 				}
204 				goto err;
205 			}
206 		} else {
207 			ZEND_ASSERT(Z_TYPE(nodes[i]) == IS_STRING);
208 
209 			newNode = xmlNewDocText(documentNode, (xmlChar *) Z_STRVAL(nodes[i]));
210 
211 			xmlSetTreeDoc(newNode, documentNode);
212 
213 			if (!xmlAddChild(fragment, newNode)) {
214 				xmlFreeNode(newNode);
215 				goto err;
216 			}
217 		}
218 	}
219 
220 	return fragment;
221 
222 err:
223 	xmlFreeNode(fragment);
224 	return NULL;
225 }
226 
dom_fragment_assign_parent_node(xmlNodePtr parentNode,xmlNodePtr fragment)227 static void dom_fragment_assign_parent_node(xmlNodePtr parentNode, xmlNodePtr fragment)
228 {
229 	xmlNodePtr node = fragment->children;
230 
231 	while (node != NULL) {
232 		node->parent = parentNode;
233 
234 		if (node == fragment->last) {
235 			break;
236 		}
237 		node = node->next;
238 	}
239 
240 	fragment->children = NULL;
241 	fragment->last = NULL;
242 }
243 
dom_sanity_check_node_list_for_insertion(php_libxml_ref_obj * document,xmlNodePtr parentNode,zval * nodes,int nodesc)244 static zend_result dom_sanity_check_node_list_for_insertion(php_libxml_ref_obj *document, xmlNodePtr parentNode, zval *nodes, int nodesc)
245 {
246 	if (document == NULL) {
247 		php_dom_throw_error(HIERARCHY_REQUEST_ERR, 1);
248 		return FAILURE;
249 	}
250 
251 	xmlDocPtr documentNode = dom_doc_from_context_node(parentNode);
252 
253 	for (int i = 0; i < nodesc; i++) {
254 		zend_uchar type = Z_TYPE(nodes[i]);
255 		if (type == IS_OBJECT) {
256 			const zend_class_entry *ce = Z_OBJCE(nodes[i]);
257 
258 			if (instanceof_function(ce, dom_node_class_entry)) {
259 				xmlNodePtr node = dom_object_get_node(Z_DOMOBJ_P(nodes + i));
260 
261 				if (node->doc != documentNode) {
262 					php_dom_throw_error(WRONG_DOCUMENT_ERR, dom_get_strict_error(document));
263 					return FAILURE;
264 				}
265 
266 				if (node->type == XML_ATTRIBUTE_NODE || dom_hierarchy(parentNode, node) != SUCCESS) {
267 					php_dom_throw_error(HIERARCHY_REQUEST_ERR, dom_get_strict_error(document));
268 					return FAILURE;
269 				}
270 			} else {
271 				zend_argument_type_error(i + 1, "must be of type DOMNode|string, %s given", zend_zval_type_name(&nodes[i]));
272 				return FAILURE;
273 			}
274 		} else if (type != IS_STRING) {
275 			zend_argument_type_error(i + 1, "must be of type DOMNode|string, %s given", zend_zval_type_name(&nodes[i]));
276 			return FAILURE;
277 		}
278 	}
279 
280 	return SUCCESS;
281 }
282 
dom_pre_insert(xmlNodePtr insertion_point,xmlNodePtr parentNode,xmlNodePtr newchild,xmlNodePtr fragment)283 static void dom_pre_insert(xmlNodePtr insertion_point, xmlNodePtr parentNode, xmlNodePtr newchild, xmlNodePtr fragment)
284 {
285 	if (!insertion_point) {
286 		/* Place it as last node */
287 		if (parentNode->children) {
288 			/* There are children */
289 			newchild->prev = parentNode->last;
290 			parentNode->last->next = newchild;
291 		} else {
292 			/* No children, because they moved out when they became a fragment */
293 			parentNode->children = newchild;
294 		}
295 		parentNode->last = fragment->last;
296 	} else {
297 		/* Insert fragment before insertion_point */
298 		fragment->last->next = insertion_point;
299 		if (insertion_point->prev) {
300 			insertion_point->prev->next = newchild;
301 			newchild->prev = insertion_point->prev;
302 		}
303 		insertion_point->prev = fragment->last;
304 		if (parentNode->children == insertion_point) {
305 			parentNode->children = newchild;
306 		}
307 	}
308 }
309 
dom_parent_node_append(dom_object * context,zval * nodes,int nodesc)310 void dom_parent_node_append(dom_object *context, zval *nodes, int nodesc)
311 {
312 	xmlNode *parentNode = dom_object_get_node(context);
313 	xmlNodePtr newchild, prevsib;
314 
315 	if (UNEXPECTED(dom_sanity_check_node_list_for_insertion(context->document, parentNode, nodes, nodesc) != SUCCESS)) {
316 		return;
317 	}
318 
319 	xmlNode *fragment = dom_zvals_to_fragment(context->document, parentNode, nodes, nodesc);
320 
321 	if (fragment == NULL) {
322 		return;
323 	}
324 
325 	newchild = fragment->children;
326 	prevsib = parentNode->last;
327 
328 	if (newchild) {
329 		if (prevsib != NULL) {
330 			prevsib->next = newchild;
331 		} else {
332 			parentNode->children = newchild;
333 		}
334 
335 		xmlNodePtr last = fragment->last;
336 		parentNode->last = last;
337 
338 		newchild->prev = prevsib;
339 
340 		dom_fragment_assign_parent_node(parentNode, fragment);
341 
342 		dom_reconcile_ns_list(parentNode->doc, newchild, last);
343 	}
344 
345 	xmlFree(fragment);
346 }
347 
dom_parent_node_prepend(dom_object * context,zval * nodes,int nodesc)348 void dom_parent_node_prepend(dom_object *context, zval *nodes, int nodesc)
349 {
350 	xmlNode *parentNode = dom_object_get_node(context);
351 
352 	if (parentNode->children == NULL) {
353 		dom_parent_node_append(context, nodes, nodesc);
354 		return;
355 	}
356 
357 	if (UNEXPECTED(dom_sanity_check_node_list_for_insertion(context->document, parentNode, nodes, nodesc) != SUCCESS)) {
358 		return;
359 	}
360 
361 	xmlNode *fragment = dom_zvals_to_fragment(context->document, parentNode, nodes, nodesc);
362 
363 	if (fragment == NULL) {
364 		return;
365 	}
366 
367 	xmlNode *newchild = fragment->children;
368 
369 	if (newchild) {
370 		xmlNodePtr last = fragment->last;
371 
372 		dom_pre_insert(parentNode->children, parentNode, newchild, fragment);
373 
374 		dom_fragment_assign_parent_node(parentNode, fragment);
375 
376 		dom_reconcile_ns_list(parentNode->doc, newchild, last);
377 	}
378 
379 	xmlFree(fragment);
380 }
381 
dom_parent_node_after(dom_object * context,zval * nodes,int nodesc)382 void dom_parent_node_after(dom_object *context, zval *nodes, int nodesc)
383 {
384 	/* Spec link: https://dom.spec.whatwg.org/#dom-childnode-after */
385 
386 	xmlNode *prevsib = dom_object_get_node(context);
387 	xmlNodePtr newchild, parentNode;
388 	xmlNode *fragment;
389 	xmlDoc *doc;
390 
391 	/* Spec step 1 */
392 	parentNode = prevsib->parent;
393 	/* Spec step 2 */
394 	if (!parentNode) {
395 		int stricterror = dom_get_strict_error(context->document);
396 		php_dom_throw_error(HIERARCHY_REQUEST_ERR, stricterror);
397 		return;
398 	}
399 
400 	/* Spec step 3: find first following child not in nodes; otherwise null */
401 	xmlNodePtr viable_next_sibling = prevsib->next;
402 	while (viable_next_sibling) {
403 		if (!dom_is_node_in_list(nodes, nodesc, viable_next_sibling)) {
404 			break;
405 		}
406 		viable_next_sibling = viable_next_sibling->next;
407 	}
408 
409 	doc = prevsib->doc;
410 
411 	if (UNEXPECTED(dom_sanity_check_node_list_for_insertion(context->document, parentNode, nodes, nodesc) != SUCCESS)) {
412 		return;
413 	}
414 
415 	/* Spec step 4: convert nodes into fragment */
416 	fragment = dom_zvals_to_fragment(context->document, parentNode, nodes, nodesc);
417 
418 	if (fragment == NULL) {
419 		return;
420 	}
421 
422 	newchild = fragment->children;
423 
424 	if (newchild) {
425 		xmlNodePtr last = fragment->last;
426 
427 		/* Step 5: place fragment into the parent before viable_next_sibling */
428 		dom_pre_insert(viable_next_sibling, parentNode, newchild, fragment);
429 
430 		dom_fragment_assign_parent_node(parentNode, fragment);
431 		dom_reconcile_ns_list(doc, newchild, last);
432 	}
433 
434 	xmlFree(fragment);
435 }
436 
dom_parent_node_before(dom_object * context,zval * nodes,int nodesc)437 void dom_parent_node_before(dom_object *context, zval *nodes, int nodesc)
438 {
439 	/* Spec link: https://dom.spec.whatwg.org/#dom-childnode-before */
440 
441 	xmlNode *nextsib = dom_object_get_node(context);
442 	xmlNodePtr newchild, parentNode;
443 	xmlNode *fragment;
444 	xmlDoc *doc;
445 
446 	/* Spec step 1 */
447 	parentNode = nextsib->parent;
448 	/* Spec step 2 */
449 	if (!parentNode) {
450 		int stricterror = dom_get_strict_error(context->document);
451 		php_dom_throw_error(HIERARCHY_REQUEST_ERR, stricterror);
452 		return;
453 	}
454 
455 	/* Spec step 3: find first following child not in nodes; otherwise null */
456 	xmlNodePtr viable_previous_sibling = nextsib->prev;
457 	while (viable_previous_sibling) {
458 		if (!dom_is_node_in_list(nodes, nodesc, viable_previous_sibling)) {
459 			break;
460 		}
461 		viable_previous_sibling = viable_previous_sibling->prev;
462 	}
463 
464 	doc = nextsib->doc;
465 
466 	if (UNEXPECTED(dom_sanity_check_node_list_for_insertion(context->document, parentNode, nodes, nodesc) != SUCCESS)) {
467 		return;
468 	}
469 
470 	/* Spec step 4: convert nodes into fragment */
471 	fragment = dom_zvals_to_fragment(context->document, parentNode, nodes, nodesc);
472 
473 	if (fragment == NULL) {
474 		return;
475 	}
476 
477 	newchild = fragment->children;
478 
479 	if (newchild) {
480 		xmlNodePtr last = fragment->last;
481 
482 		/* Step 5: if viable_previous_sibling is null, set it to the parent's first child, otherwise viable_previous_sibling's next sibling */
483 		if (!viable_previous_sibling) {
484 			viable_previous_sibling = parentNode->children;
485 		} else {
486 			viable_previous_sibling = viable_previous_sibling->next;
487 		}
488 		/* Step 6: place fragment into the parent after viable_previous_sibling */
489 		dom_pre_insert(viable_previous_sibling, parentNode, newchild, fragment);
490 
491 		dom_fragment_assign_parent_node(parentNode, fragment);
492 		dom_reconcile_ns_list(doc, newchild, last);
493 	}
494 
495 	xmlFree(fragment);
496 }
497 
dom_child_removal_preconditions(const xmlNodePtr child,int stricterror)498 static zend_result dom_child_removal_preconditions(const xmlNodePtr child, int stricterror)
499 {
500 	if (dom_node_is_read_only(child) == SUCCESS ||
501 		(child->parent != NULL && dom_node_is_read_only(child->parent) == SUCCESS)) {
502 		php_dom_throw_error(NO_MODIFICATION_ALLOWED_ERR, stricterror);
503 		return FAILURE;
504 	}
505 
506 	if (!child->parent) {
507 		php_dom_throw_error(NOT_FOUND_ERR, stricterror);
508 		return FAILURE;
509 	}
510 
511 	if (dom_node_children_valid(child->parent) == FAILURE) {
512 		return FAILURE;
513 	}
514 
515 	xmlNodePtr children = child->parent->children;
516 	if (!children) {
517 		php_dom_throw_error(NOT_FOUND_ERR, stricterror);
518 		return FAILURE;
519 	}
520 
521 	return SUCCESS;
522 }
523 
dom_child_node_remove(dom_object * context)524 void dom_child_node_remove(dom_object *context)
525 {
526 	xmlNode *child = dom_object_get_node(context);
527 	xmlNodePtr children;
528 	int stricterror;
529 
530 	stricterror = dom_get_strict_error(context->document);
531 
532 	if (UNEXPECTED(dom_child_removal_preconditions(child, stricterror) != SUCCESS)) {
533 		return;
534 	}
535 
536 	children = child->parent->children;
537 	while (children) {
538 		if (children == child) {
539 			xmlUnlinkNode(child);
540 			return;
541 		}
542 		children = children->next;
543 	}
544 
545 	php_dom_throw_error(NOT_FOUND_ERR, stricterror);
546 }
547 
dom_child_replace_with(dom_object * context,zval * nodes,int nodesc)548 void dom_child_replace_with(dom_object *context, zval *nodes, int nodesc)
549 {
550 	/* Spec link: https://dom.spec.whatwg.org/#dom-childnode-replacewith */
551 
552 	xmlNodePtr child = dom_object_get_node(context);
553 
554 	/* Spec step 1 */
555 	xmlNodePtr parentNode = child->parent;
556 	/* Spec step 2 */
557 	if (!parentNode) {
558 		int stricterror = dom_get_strict_error(context->document);
559 		php_dom_throw_error(HIERARCHY_REQUEST_ERR, stricterror);
560 		return;
561 	}
562 
563 	int stricterror = dom_get_strict_error(context->document);
564 	if (UNEXPECTED(dom_child_removal_preconditions(child, stricterror) != SUCCESS)) {
565 		return;
566 	}
567 
568 	/* Spec step 3: find first following child not in nodes; otherwise null */
569 	xmlNodePtr viable_next_sibling = child->next;
570 	while (viable_next_sibling) {
571 		if (!dom_is_node_in_list(nodes, nodesc, viable_next_sibling)) {
572 			break;
573 		}
574 		viable_next_sibling = viable_next_sibling->next;
575 	}
576 
577 	if (UNEXPECTED(dom_sanity_check_node_list_for_insertion(context->document, parentNode, nodes, nodesc) != SUCCESS)) {
578 		return;
579 	}
580 
581 	/* Spec step 4: convert nodes into fragment */
582 	xmlNodePtr fragment = dom_zvals_to_fragment(context->document, parentNode, nodes, nodesc);
583 	if (UNEXPECTED(fragment == NULL)) {
584 		return;
585 	}
586 
587 	/* Spec step 5: perform the replacement */
588 
589 	xmlNodePtr newchild = fragment->children;
590 	xmlDocPtr doc = parentNode->doc;
591 
592 	/* Unlink and free it unless it became a part of the fragment. */
593 	if (child->parent != fragment) {
594 		xmlUnlinkNode(child);
595 	}
596 
597 	if (newchild) {
598 		xmlNodePtr last = fragment->last;
599 
600 		dom_pre_insert(viable_next_sibling, parentNode, newchild, fragment);
601 
602 		dom_fragment_assign_parent_node(parentNode, fragment);
603 		dom_reconcile_ns_list(doc, newchild, last);
604 	}
605 
606 	xmlFree(fragment);
607 }
608 
609 #endif
610