xref: /php-src/ext/dom/parentnode/tree.c (revision 65fbf474)
1 /*
2    +----------------------------------------------------------------------+
3    | Copyright (c) The PHP Group                                          |
4    +----------------------------------------------------------------------+
5    | This source file is subject to version 3.01 of the PHP license,      |
6    | that is bundled with this package in the file LICENSE, and is        |
7    | available through the world-wide-web at the following url:           |
8    | https://www.php.net/license/3_01.txt                                 |
9    | If you did not receive a copy of the PHP license and are unable to   |
10    | obtain it through the world-wide-web, please send a note to          |
11    | license@php.net so we can mail you a copy immediately.               |
12    +----------------------------------------------------------------------+
13    | Authors: Benjamin Eberlei <beberlei@php.net>                         |
14    |          Niels Dossche <nielsdos@php.net>                            |
15    +----------------------------------------------------------------------+
16 */
17 
18 #ifdef HAVE_CONFIG_H
19 #include <config.h>
20 #endif
21 
22 #include "php.h"
23 #if defined(HAVE_LIBXML) && defined(HAVE_DOM)
24 #include "../php_dom.h"
25 #include "../internal_helpers.h"
26 #include "../dom_properties.h"
27 
28 /* {{{ firstElementChild DomParentNode
29 readonly=yes
30 URL: https://www.w3.org/TR/dom/#dom-parentnode-firstelementchild
31 */
dom_parent_node_first_element_child_read(dom_object * obj,zval * retval)32 zend_result dom_parent_node_first_element_child_read(dom_object *obj, zval *retval)
33 {
34 	DOM_PROP_NODE(xmlNodePtr, nodep, obj);
35 
36 	xmlNodePtr first = nodep->children;
37 
38 	while (first && first->type != XML_ELEMENT_NODE) {
39 		first = first->next;
40 	}
41 
42 	php_dom_create_nullable_object(first, retval, obj);
43 	return SUCCESS;
44 }
45 /* }}} */
46 
47 /* {{{ lastElementChild DomParentNode
48 readonly=yes
49 URL: https://www.w3.org/TR/dom/#dom-parentnode-lastelementchild
50 */
dom_parent_node_last_element_child_read(dom_object * obj,zval * retval)51 zend_result dom_parent_node_last_element_child_read(dom_object *obj, zval *retval)
52 {
53 	DOM_PROP_NODE(xmlNodePtr, nodep, obj);
54 
55 	xmlNodePtr last = nodep->last;
56 
57 	while (last && last->type != XML_ELEMENT_NODE) {
58 		last = last->prev;
59 	}
60 
61 	php_dom_create_nullable_object(last, retval, obj);
62 	return SUCCESS;
63 }
64 /* }}} */
65 
66 /* {{{ childElementCount DomParentNode
67 readonly=yes
68 https://www.w3.org/TR/dom/#dom-parentnode-childelementcount
69 */
dom_parent_node_child_element_count(dom_object * obj,zval * retval)70 zend_result dom_parent_node_child_element_count(dom_object *obj, zval *retval)
71 {
72 	DOM_PROP_NODE(xmlNodePtr, nodep, obj);
73 
74 	zend_long count = 0;
75 	xmlNodePtr first = nodep->children;
76 
77 	while (first != NULL) {
78 		if (first->type == XML_ELEMENT_NODE) {
79 			count++;
80 		}
81 
82 		first = first->next;
83 	}
84 
85 	ZVAL_LONG(retval, count);
86 
87 	return SUCCESS;
88 }
89 /* }}} */
90 
dom_cannot_create_temp_nodes(void)91 static ZEND_COLD void dom_cannot_create_temp_nodes(void)
92 {
93 	php_dom_throw_error_with_message(INVALID_MODIFICATION_ERR, "Unable to allocate temporary nodes", /* strict */ true);
94 }
95 
dom_is_node_in_list(const zval * nodes,uint32_t nodesc,const xmlNode * node_to_find)96 static bool dom_is_node_in_list(const zval *nodes, uint32_t nodesc, const xmlNode *node_to_find)
97 {
98 	for (uint32_t i = 0; i < nodesc; i++) {
99 		if (Z_TYPE(nodes[i]) == IS_OBJECT) {
100 			if (dom_object_get_node(Z_DOMOBJ_P(nodes + i)) == node_to_find) {
101 				return true;
102 			}
103 		}
104 	}
105 
106 	return false;
107 }
108 
dom_doc_from_context_node(xmlNodePtr contextNode)109 static xmlDocPtr dom_doc_from_context_node(xmlNodePtr contextNode)
110 {
111 	if (contextNode->type == XML_DOCUMENT_NODE || contextNode->type == XML_HTML_DOCUMENT_NODE) {
112 		return (xmlDocPtr) contextNode;
113 	} else {
114 		return contextNode->doc;
115 	}
116 }
117 
118 /* Citing from the docs (https://gnome.pages.gitlab.gnome.org/libxml2/devhelp/libxml2-tree.html#xmlAddChild):
119  * "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)".
120  * So we must use a custom way of adding that does not merge. */
dom_add_child_without_merging(xmlNodePtr parent,xmlNodePtr child)121 static void dom_add_child_without_merging(xmlNodePtr parent, xmlNodePtr child)
122 {
123 	if (parent->children == NULL) {
124 		parent->children = child;
125 	} else {
126 		xmlNodePtr last = parent->last;
127 		last->next = child;
128 		child->prev = last;
129 	}
130 	parent->last = child;
131 	child->parent = parent;
132 }
133 
dom_fragment_assign_parent_node(xmlNodePtr parentNode,xmlNodePtr fragment)134 static void dom_fragment_assign_parent_node(xmlNodePtr parentNode, xmlNodePtr fragment)
135 {
136 	xmlNodePtr node = fragment->children;
137 
138 	while (node != NULL) {
139 		node->parent = parentNode;
140 
141 		if (node == fragment->last) {
142 			break;
143 		}
144 		node = node->next;
145 	}
146 }
147 
148 /* This part is common logic between the pre-insertion validity and replaceChild code. */
dom_fragment_common_hierarchy_check_part(xmlNodePtr node,bool * seen_element)149 static bool dom_fragment_common_hierarchy_check_part(xmlNodePtr node, bool *seen_element)
150 {
151 	/* If node has more than one element child or has a Text node child. */
152 	xmlNodePtr iter = node->children;
153 	*seen_element = false;
154 	while (iter != NULL) {
155 		if (iter->type == XML_ELEMENT_NODE) {
156 			if (*seen_element) {
157 				php_dom_throw_error_with_message(HIERARCHY_REQUEST_ERR, "Cannot have more than one element child in a document", /* strict */ true);
158 				return false;
159 			}
160 			*seen_element = true;
161 		} else if (iter->type == XML_TEXT_NODE || iter->type == XML_CDATA_SECTION_NODE) {
162 			php_dom_throw_error_with_message(HIERARCHY_REQUEST_ERR, "Cannot insert text as a child of a document", /* strict */ true);
163 			return false;
164 		}
165 		iter = iter->next;
166 	}
167 	return true;
168 }
169 
170 /* https://dom.spec.whatwg.org/#concept-node-ensure-pre-insertion-validity
171  * DocumentFragment validation part. */
php_dom_fragment_insertion_hierarchy_check_pre_insertion(xmlNodePtr parent,xmlNodePtr node,xmlNodePtr child)172 bool php_dom_fragment_insertion_hierarchy_check_pre_insertion(xmlNodePtr parent, xmlNodePtr node, xmlNodePtr child)
173 {
174 	bool seen_element;
175 	if (!dom_fragment_common_hierarchy_check_part(node, &seen_element)) {
176 		return false;
177 	}
178 
179 	/* Otherwise, if node has one element child
180 	 * and either parent has an element child, child is a doctype, or child is non-null and a doctype is following child. */
181 	if (seen_element) {
182 		if (php_dom_has_child_of_type(parent, XML_ELEMENT_NODE)) {
183 			php_dom_throw_error_with_message(HIERARCHY_REQUEST_ERR, "Cannot have more than one element child in a document", /* strict */ true);
184 			return false;
185 		}
186 
187 		if (child != NULL && (child->type == XML_DTD_NODE || php_dom_has_sibling_following_node(child, XML_DTD_NODE))) {
188 			php_dom_throw_error_with_message(HIERARCHY_REQUEST_ERR, "Document types must be the first child in a document", /* strict */ true);
189 			return false;
190 		}
191 	}
192 
193 	return true;
194 }
195 
196 /* https://dom.spec.whatwg.org/#concept-node-replace
197  * DocumentFragment validation part. */
php_dom_fragment_insertion_hierarchy_check_replace(xmlNodePtr parent,xmlNodePtr node,xmlNodePtr child)198 bool php_dom_fragment_insertion_hierarchy_check_replace(xmlNodePtr parent, xmlNodePtr node, xmlNodePtr child)
199 {
200 	bool seen_element;
201 	if (!dom_fragment_common_hierarchy_check_part(node, &seen_element)) {
202 		return false;
203 	}
204 
205 	/* Otherwise, if node has one element child
206 	 * and either parent has an element child that is not child or a doctype is following child. */
207 	if (seen_element) {
208 		xmlNodePtr iter = parent->children;
209 		while (iter != NULL) {
210 			if (iter->type == XML_ELEMENT_NODE && iter != child) {
211 				php_dom_throw_error_with_message(HIERARCHY_REQUEST_ERR, "Cannot have more than one element child in a document", /* strict */ true);
212 				return false;
213 			}
214 			iter = iter->next;
215 		}
216 
217 		ZEND_ASSERT(child != NULL);
218 		if (php_dom_has_sibling_following_node(child, XML_DTD_NODE)) {
219 			php_dom_throw_error_with_message(HIERARCHY_REQUEST_ERR, "Document types must be the first child in a document", /* strict */ true);
220 			return false;
221 		}
222 	}
223 
224 	return true;
225 }
226 
227 /* https://dom.spec.whatwg.org/#concept-node-ensure-pre-insertion-validity */
php_dom_pre_insert_is_parent_invalid(xmlNodePtr parent)228 bool php_dom_pre_insert_is_parent_invalid(xmlNodePtr parent)
229 {
230 	return parent->type != XML_DOCUMENT_NODE
231 		&& parent->type != XML_HTML_DOCUMENT_NODE
232 		&& parent->type != XML_ELEMENT_NODE
233 		&& parent->type != XML_DOCUMENT_FRAG_NODE;
234 }
235 
236 /* https://dom.spec.whatwg.org/#concept-node-ensure-pre-insertion-validity */
dom_is_pre_insert_valid_without_step_1(php_libxml_ref_obj * document,xmlNodePtr parentNode,xmlNodePtr node,xmlNodePtr child,xmlDocPtr documentNode)237 static bool dom_is_pre_insert_valid_without_step_1(php_libxml_ref_obj *document, xmlNodePtr parentNode, xmlNodePtr node, xmlNodePtr child, xmlDocPtr documentNode)
238 {
239 	ZEND_ASSERT(parentNode != NULL);
240 
241 	/* 1. If parent is not a Document, DocumentFragment, or Element node, then throw a "HierarchyRequestError" DOMException.
242 	 *    => This is possible because we can grab children of attributes etc... (see e.g. GH-16594) */
243 	if (php_dom_pre_insert_is_parent_invalid(parentNode)) {
244 		php_dom_throw_error(HIERARCHY_REQUEST_ERR, dom_get_strict_error(document));
245 		return false;
246 	}
247 
248 	if (node->doc != documentNode) {
249 		php_dom_throw_error(WRONG_DOCUMENT_ERR, dom_get_strict_error(document));
250 		return false;
251 	}
252 
253 	/* 3. If child is non-null and its parent is not parent, then throw a "NotFoundError" DOMException. */
254 	if (child != NULL && child->parent != parentNode) {
255 		php_dom_throw_error(NOT_FOUND_ERR, dom_get_strict_error(document));
256 		return false;
257 	}
258 
259 	bool parent_is_document = parentNode->type == XML_DOCUMENT_NODE || parentNode->type == XML_HTML_DOCUMENT_NODE;
260 
261 	if (/* 2. If node is a host-including inclusive ancestor of parent, then throw a "HierarchyRequestError" DOMException. */
262 		dom_hierarchy(parentNode, node) != SUCCESS
263 		/* 4. If node is not a DocumentFragment, DocumentType, Element, or CharacterData node, then throw a "HierarchyRequestError" DOMException. */
264 		|| node->type == XML_ATTRIBUTE_NODE
265 		|| (php_dom_follow_spec_doc_ref(document) && (
266 			node->type == XML_ENTITY_REF_NODE
267 			|| node->type == XML_ENTITY_NODE
268 			|| node->type == XML_NOTATION_NODE
269 			|| node->type == XML_DOCUMENT_NODE
270 			|| node->type == XML_HTML_DOCUMENT_NODE
271 			|| node->type >= XML_ELEMENT_DECL))) {
272 		php_dom_throw_error(HIERARCHY_REQUEST_ERR, dom_get_strict_error(document));
273 		return false;
274 	}
275 
276 	if (php_dom_follow_spec_doc_ref(document)) {
277 		/* 5. If either node is a Text node and parent is a document... */
278 		if (parent_is_document && (node->type == XML_TEXT_NODE || node->type == XML_CDATA_SECTION_NODE)) {
279 			php_dom_throw_error_with_message(HIERARCHY_REQUEST_ERR, "Cannot insert text as a child of a document", /* strict */ true);
280 			return false;
281 		}
282 
283 		/* 5. ..., or node is a doctype and parent is not a document, then throw a "HierarchyRequestError" DOMException. */
284 		if (!parent_is_document && node->type == XML_DTD_NODE) {
285 			php_dom_throw_error_with_message(HIERARCHY_REQUEST_ERR, "Cannot insert a document type into anything other than a document", /* strict */ true);
286 			return false;
287 		}
288 
289 		/* 6. If parent is a document, and any of the statements below, switched on the interface node implements,
290 		 *    are true, then throw a "HierarchyRequestError" DOMException. */
291 		if (parent_is_document) {
292 			/* DocumentFragment */
293 			if (node->type == XML_DOCUMENT_FRAG_NODE) {
294 				if (!php_dom_fragment_insertion_hierarchy_check_pre_insertion(parentNode, node, child)) {
295 					return false;
296 				}
297 			}
298 			/* Element */
299 			else if (node->type == XML_ELEMENT_NODE) {
300 				/* parent has an element child, child is a doctype, or child is non-null and a doctype is following child. */
301 				if (php_dom_has_child_of_type(parentNode, XML_ELEMENT_NODE)) {
302 					php_dom_throw_error_with_message(HIERARCHY_REQUEST_ERR, "Cannot have more than one element child in a document", /* strict */ true);
303 					return false;
304 				}
305 				if (child != NULL && (child->type == XML_DTD_NODE || php_dom_has_sibling_following_node(child, XML_DTD_NODE))) {
306 					php_dom_throw_error_with_message(HIERARCHY_REQUEST_ERR, "Document types must be the first child in a document", /* strict */ true);
307 					return false;
308 				}
309 			}
310 			/* DocumentType */
311 			else if (node->type == XML_DTD_NODE) {
312 				/* parent has a doctype child, child is non-null and an element is preceding child, or child is null and parent has an element child. */
313 				if (php_dom_has_child_of_type(parentNode, XML_DTD_NODE)) {
314 					php_dom_throw_error_with_message(HIERARCHY_REQUEST_ERR, "Cannot have more than one document type", /* strict */ true);
315 					return false;
316 				}
317 				if ((child != NULL && php_dom_has_sibling_preceding_node(child, XML_ELEMENT_NODE))
318 					|| (child == NULL && php_dom_has_child_of_type(parentNode, XML_ELEMENT_NODE))) {
319 					php_dom_throw_error_with_message(HIERARCHY_REQUEST_ERR, "Document types must be the first child in a document", /* strict */ true);
320 					return false;
321 				}
322 			}
323 		}
324 	}
325 
326 	return true;
327 }
328 
dom_free_node_after_zval_single_node_creation(xmlNodePtr node)329 static void dom_free_node_after_zval_single_node_creation(xmlNodePtr node)
330 {
331 	/* For the object cases, the user did provide them, so they don't have to be freed as there are still references.
332 	 * For the newly created text nodes, we do have to free them. */
333 	xmlNodePtr next;
334 	for (xmlNodePtr child = node->children; child != NULL; child = next) {
335 		next = child->next;
336 		xmlUnlinkNode(child);
337 		if (child->_private == NULL) {
338 			xmlFreeNode(child);
339 		}
340 	}
341 }
342 
343 /* https://dom.spec.whatwg.org/#converting-nodes-into-a-node */
dom_zvals_to_single_node(php_libxml_ref_obj * document,xmlNode * contextNode,zval * nodes,uint32_t nodesc)344 xmlNode* dom_zvals_to_single_node(php_libxml_ref_obj *document, xmlNode *contextNode, zval *nodes, uint32_t nodesc)
345 {
346 	xmlDoc *documentNode;
347 	xmlNode *newNode;
348 	dom_object *newNodeObj;
349 
350 	documentNode = dom_doc_from_context_node(contextNode);
351 
352 	/* 1. Let node be null. */
353 	xmlNodePtr node = NULL;
354 
355 	/* 2. => handled in the loop. */
356 
357 	/* 3. If nodes contains one node, then set node to nodes[0]. */
358 	if (nodesc == 1) {
359 		/* ... and return */
360 		if (Z_TYPE_P(nodes) == IS_OBJECT) {
361 			return dom_object_get_node(Z_DOMOBJ_P(nodes));
362 		} else {
363 			ZEND_ASSERT(Z_TYPE_P(nodes) == IS_STRING);
364 			node = xmlNewDocTextLen(documentNode, BAD_CAST Z_STRVAL_P(nodes), Z_STRLEN_P(nodes));
365 			if (UNEXPECTED(node == NULL)) {
366 				dom_cannot_create_temp_nodes();
367 			}
368 			return node;
369 		}
370 	}
371 
372 	node = xmlNewDocFragment(documentNode);
373 	if (UNEXPECTED(!node)) {
374 		dom_cannot_create_temp_nodes();
375 		return NULL;
376 	}
377 
378 	/* 4. Otherwise, set node to a new DocumentFragment node whose node document is document,
379 	 *    and then append each node in nodes, if any, to it. */
380 	for (uint32_t i = 0; i < nodesc; i++) {
381 		if (Z_TYPE(nodes[i]) == IS_OBJECT) {
382 			newNodeObj = Z_DOMOBJ_P(&nodes[i]);
383 			newNode = dom_object_get_node(newNodeObj);
384 
385 			if (UNEXPECTED(!newNode)) {
386 				php_dom_throw_error(INVALID_STATE_ERR, /* strict */ true);
387 				goto err;
388 			}
389 
390 			if (!dom_is_pre_insert_valid_without_step_1(document, node, newNode, NULL, documentNode)) {
391 				goto err;
392 			}
393 
394 			if (newNode->parent != NULL) {
395 				xmlUnlinkNode(newNode);
396 			}
397 
398 			ZEND_ASSERT(newNodeObj->document == document);
399 
400 			if (newNode->type == XML_DOCUMENT_FRAG_NODE) {
401 				/* Unpack document fragment nodes, the behaviour differs for different libxml2 versions. */
402 				newNode = newNode->children;
403 				while (newNode) {
404 					xmlNodePtr next = newNode->next;
405 					xmlUnlinkNode(newNode);
406 					dom_add_child_without_merging(node, newNode);
407 					newNode = next;
408 				}
409 			} else {
410 				dom_add_child_without_merging(node, newNode);
411 			}
412 		} else {
413 			/* 2. Replace each string in nodes with a new Text node whose data is the string and node document is document. */
414 			ZEND_ASSERT(Z_TYPE(nodes[i]) == IS_STRING);
415 
416 			/* Text nodes can't violate the hierarchy at this point. */
417 			newNode = xmlNewDocTextLen(documentNode, BAD_CAST Z_STRVAL(nodes[i]), Z_STRLEN(nodes[i]));
418 			if (UNEXPECTED(newNode == NULL)) {
419 				dom_cannot_create_temp_nodes();
420 				goto err;
421 			}
422 			dom_add_child_without_merging(node, newNode);
423 		}
424 	}
425 
426 	/* 5. Return node. */
427 	return node;
428 
429 err:
430 	/* For the object cases, the user did provide them, so they don't have to be freed as there are still references.
431 	 * For the newly created text nodes, we do have to free them. */
432 	dom_free_node_after_zval_single_node_creation(node);
433 	xmlFree(node);
434 	return NULL;
435 }
436 
dom_sanity_check_node_list_types(zval * nodes,uint32_t nodesc,zend_class_entry * node_ce)437 static zend_result dom_sanity_check_node_list_types(zval *nodes, uint32_t nodesc, zend_class_entry *node_ce)
438 {
439 	for (uint32_t i = 0; i < nodesc; i++) {
440 		zend_uchar type = Z_TYPE(nodes[i]);
441 		if (type == IS_OBJECT) {
442 			const zend_class_entry *ce = Z_OBJCE(nodes[i]);
443 
444 			if (!instanceof_function(ce, node_ce)) {
445 				zend_argument_type_error(i + 1, "must be of type %s|string, %s given", ZSTR_VAL(node_ce->name), zend_zval_type_name(&nodes[i]));
446 				return FAILURE;
447 			}
448 		} else if (type == IS_STRING) {
449 			if (Z_STRLEN(nodes[i]) > INT_MAX) {
450 				zend_argument_value_error(i + 1, "must be less than or equal to %d bytes long", INT_MAX);
451 				return FAILURE;
452 			}
453 		} else {
454 			zend_argument_type_error(i + 1, "must be of type %s|string, %s given", ZSTR_VAL(node_ce->name), zend_zval_type_name(&nodes[i]));
455 			return FAILURE;
456 		}
457 	}
458 
459 	return SUCCESS;
460 }
461 
php_dom_pre_insert_helper(xmlNodePtr insertion_point,xmlNodePtr parentNode,xmlNodePtr newchild,xmlNodePtr last)462 static void php_dom_pre_insert_helper(xmlNodePtr insertion_point, xmlNodePtr parentNode, xmlNodePtr newchild, xmlNodePtr last)
463 {
464 	if (!insertion_point) {
465 		/* Place it as last node */
466 		if (parentNode->children) {
467 			/* There are children */
468 			newchild->prev = parentNode->last;
469 			parentNode->last->next = newchild;
470 		} else {
471 			/* No children, because they moved out when they became a fragment */
472 			parentNode->children = newchild;
473 		}
474 		parentNode->last = last;
475 	} else {
476 		/* Insert fragment before insertion_point */
477 		last->next = insertion_point;
478 		if (insertion_point->prev) {
479 			insertion_point->prev->next = newchild;
480 			newchild->prev = insertion_point->prev;
481 		}
482 		insertion_point->prev = last;
483 		if (parentNode->children == insertion_point) {
484 			parentNode->children = newchild;
485 		}
486 	}
487 }
488 
dom_insert_node_list_cleanup(xmlNodePtr node)489 static void dom_insert_node_list_cleanup(xmlNodePtr node)
490 {
491 	if (node->_private != NULL) {
492 		/* Not a temporary node. */
493 		return;
494 	}
495 	if (node->type == XML_DOCUMENT_FRAG_NODE) {
496 		dom_free_node_after_zval_single_node_creation(node);
497 		xmlFree(node); /* Don't free the children, now-empty fragment! */
498 	} else if (node->type == XML_TEXT_NODE) {
499 		ZEND_ASSERT(node->parent == NULL);
500 		xmlFreeNode(node);
501 	} else {
502 		/* Must have been a directly-passed node. */
503 		ZEND_UNREACHABLE();
504 	}
505 }
506 
507 /* https://dom.spec.whatwg.org/#concept-node-pre-insert */
dom_insert_node_list_unchecked(php_libxml_ref_obj * document,xmlNodePtr node,xmlNodePtr parent,xmlNodePtr insertion_point)508 static void dom_insert_node_list_unchecked(php_libxml_ref_obj *document, xmlNodePtr node, xmlNodePtr parent, xmlNodePtr insertion_point)
509 {
510 	/* Step 1 should be checked by the caller. */
511 
512 	if (node->type == XML_DOCUMENT_FRAG_NODE) {
513 		/* Steps 2-3 are not applicable here, the condition is impossible. */
514 
515 		xmlNodePtr newchild = node->children;
516 
517 		/* 4. Insert node into parent before referenceChild (i.e. insertion_point here because of the impossible condition). */
518 		if (newchild) {
519 			xmlNodePtr last = node->last;
520 			php_dom_pre_insert_helper(insertion_point, parent, newchild, last);
521 			dom_fragment_assign_parent_node(parent, node);
522 			if (!php_dom_follow_spec_doc_ref(document)) {
523 				dom_reconcile_ns_list(parent->doc, newchild, last);
524 			}
525 			if (parent->doc && newchild->type == XML_DTD_NODE) {
526 				parent->doc->intSubset = (xmlDtdPtr) newchild;
527 				newchild->parent = (xmlNodePtr) parent->doc;
528 			}
529 		}
530 
531 		if (node->_private == NULL) {
532 			xmlFree(node);
533 		} else {
534 			node->children = NULL;
535 			node->last = NULL;
536 		}
537 	} else {
538 		/* 2. Let referenceChild be child.
539 		 * 3. If referenceChild is node, then set referenceChild to node’s next sibling. */
540 		if (insertion_point == node) {
541 			insertion_point = node->next;
542 		}
543 
544 		/* 4. Insert node into parent before referenceChild. */
545 		xmlUnlinkNode(node);
546 		php_dom_pre_insert_helper(insertion_point, parent, node, node);
547 		node->parent = parent;
548 		if (parent->doc && node->type == XML_DTD_NODE) {
549 			parent->doc->intSubset = (xmlDtdPtr) node;
550 			node->parent = (xmlNodePtr) parent->doc;
551 		} else {
552 			if (!php_dom_follow_spec_doc_ref(document)) {
553 				dom_reconcile_ns(parent->doc, node);
554 			}
555 		}
556 	}
557 }
558 
559 /* https://dom.spec.whatwg.org/#concept-node-pre-insert */
php_dom_pre_insert(php_libxml_ref_obj * document,xmlNodePtr node,xmlNodePtr parent,xmlNodePtr insertion_point)560 bool php_dom_pre_insert(php_libxml_ref_obj *document, xmlNodePtr node, xmlNodePtr parent, xmlNodePtr insertion_point)
561 {
562 	if (UNEXPECTED(node == NULL)) {
563 		return false;
564 	}
565 
566 	/* Step 1 checked here, other steps delegated to other function. */
567 	if (dom_is_pre_insert_valid_without_step_1(document, parent, node, insertion_point, parent->doc)) {
568 		dom_insert_node_list_unchecked(document, node, parent, insertion_point);
569 		return true;
570 	} else {
571 		dom_insert_node_list_cleanup(node);
572 		return false;
573 	}
574 }
575 
576 /* https://dom.spec.whatwg.org/#concept-node-append */
php_dom_node_append(php_libxml_ref_obj * document,xmlNodePtr node,xmlNodePtr parent)577 void php_dom_node_append(php_libxml_ref_obj *document, xmlNodePtr node, xmlNodePtr parent)
578 {
579 	php_dom_pre_insert(document, node, parent, NULL);
580 }
581 
582 /* https://dom.spec.whatwg.org/#dom-parentnode-append */
dom_parent_node_append(dom_object * context,zval * nodes,uint32_t nodesc)583 void dom_parent_node_append(dom_object *context, zval *nodes, uint32_t nodesc)
584 {
585 	if (UNEXPECTED(dom_sanity_check_node_list_types(nodes, nodesc, dom_get_node_ce(php_dom_follow_spec_doc_ref(context->document))) != SUCCESS)) {
586 		return;
587 	}
588 
589 	xmlNode *parentNode = dom_object_get_node(context);
590 
591 	php_libxml_invalidate_node_list_cache(context->document);
592 
593 	/* 1. Let node be the result of converting nodes into a node given nodes and this’s node document. */
594 	xmlNodePtr node = dom_zvals_to_single_node(context->document, parentNode, nodes, nodesc);
595 	if (UNEXPECTED(node == NULL)) {
596 		return;
597 	}
598 
599 	/* 2. Append node to this. */
600 	php_dom_node_append(context->document, node, parentNode);
601 }
602 
603 /* https://dom.spec.whatwg.org/#dom-parentnode-prepend */
dom_parent_node_prepend(dom_object * context,zval * nodes,uint32_t nodesc)604 void dom_parent_node_prepend(dom_object *context, zval *nodes, uint32_t nodesc)
605 {
606 	xmlNode *parentNode = dom_object_get_node(context);
607 
608 	if (parentNode->children == NULL) {
609 		dom_parent_node_append(context, nodes, nodesc);
610 		return;
611 	}
612 
613 	if (UNEXPECTED(dom_sanity_check_node_list_types(nodes, nodesc, dom_get_node_ce(php_dom_follow_spec_doc_ref(context->document))) != SUCCESS)) {
614 		return;
615 	}
616 
617 	php_libxml_invalidate_node_list_cache(context->document);
618 
619 	/* 1. Let node be the result of converting nodes into a node given nodes and this’s node document. */
620 	xmlNodePtr node = dom_zvals_to_single_node(context->document, parentNode, nodes, nodesc);
621 	if (UNEXPECTED(node == NULL)) {
622 		return;
623 	}
624 
625 	/* 2. Pre-insert node into this before this’s first child. */
626 	php_dom_pre_insert(context->document, node, parentNode, parentNode->children);
627 }
628 
629 /* https://dom.spec.whatwg.org/#dom-childnode-after */
dom_parent_node_after(dom_object * context,zval * nodes,uint32_t nodesc)630 void dom_parent_node_after(dom_object *context, zval *nodes, uint32_t nodesc)
631 {
632 	if (UNEXPECTED(dom_sanity_check_node_list_types(nodes, nodesc, dom_get_node_ce(php_dom_follow_spec_doc_ref(context->document))) != SUCCESS)) {
633 		return;
634 	}
635 
636 	xmlNode *thisp = dom_object_get_node(context);
637 
638 	/* 1. Let parent be this’s parent. */
639 	xmlNodePtr parentNode = thisp->parent;
640 
641 	/* 2. If parent is null, then return. */
642 	if (UNEXPECTED(parentNode == NULL)) {
643 		return;
644 	}
645 
646 	/* 3. Let viableNextSibling be this’s first following sibling not in nodes; otherwise null. */
647 	xmlNodePtr viable_next_sibling = thisp->next;
648 	while (viable_next_sibling && dom_is_node_in_list(nodes, nodesc, viable_next_sibling)) {
649 		viable_next_sibling = viable_next_sibling->next;
650 	}
651 
652 	php_libxml_invalidate_node_list_cache(context->document);
653 
654 	/* 4. Let node be the result of converting nodes into a node, given nodes and this’s node document. */
655 	xmlNodePtr fragment = dom_zvals_to_single_node(context->document, parentNode, nodes, nodesc);
656 
657 	/* 5. Pre-insert node into parent before viableNextSibling. */
658 	php_dom_pre_insert(context->document, fragment, parentNode, viable_next_sibling);
659 }
660 
661 /* https://dom.spec.whatwg.org/#dom-childnode-before */
dom_parent_node_before(dom_object * context,zval * nodes,uint32_t nodesc)662 void dom_parent_node_before(dom_object *context, zval *nodes, uint32_t nodesc)
663 {
664 	if (UNEXPECTED(dom_sanity_check_node_list_types(nodes, nodesc, dom_get_node_ce(php_dom_follow_spec_doc_ref(context->document))) != SUCCESS)) {
665 		return;
666 	}
667 
668 	xmlNode *thisp = dom_object_get_node(context);
669 
670 	/* 1. Let parent be this’s parent. */
671 	xmlNodePtr parentNode = thisp->parent;
672 
673 	/* 2. If parent is null, then return. */
674 	if (UNEXPECTED(parentNode == NULL)) {
675 		return;
676 	}
677 
678 	/* 3. Let viablePreviousSibling be this’s first preceding sibling not in nodes; otherwise null. */
679 	xmlNodePtr viable_previous_sibling = thisp->prev;
680 	while (viable_previous_sibling && dom_is_node_in_list(nodes, nodesc, viable_previous_sibling)) {
681 		viable_previous_sibling = viable_previous_sibling->prev;
682 	}
683 
684 	php_libxml_invalidate_node_list_cache(context->document);
685 
686 	/* 4. Let node be the result of converting nodes into a node, given nodes and this’s node document. */
687 	xmlNodePtr fragment = dom_zvals_to_single_node(context->document, parentNode, nodes, nodesc);
688 
689 	/* 5. If viable_previous_sibling is null, set it to the parent's first child, otherwise viable_previous_sibling's next sibling. */
690 	if (!viable_previous_sibling) {
691 		viable_previous_sibling = parentNode->children;
692 	} else {
693 		viable_previous_sibling = viable_previous_sibling->next;
694 	}
695 
696 	/* 6. Pre-insert node into parent before viablePreviousSibling. */
697 	php_dom_pre_insert(context->document, fragment, parentNode, viable_previous_sibling);
698 }
699 
dom_child_removal_preconditions(const xmlNode * child,const dom_object * context)700 static zend_result dom_child_removal_preconditions(const xmlNode *child, const dom_object *context)
701 {
702 	if (dom_node_is_read_only(child) == SUCCESS ||
703 		(child->parent != NULL && dom_node_is_read_only(child->parent) == SUCCESS)) {
704 		php_dom_throw_error(NO_MODIFICATION_ALLOWED_ERR, dom_get_strict_error(context->document));
705 		return FAILURE;
706 	}
707 
708 	if (!child->parent) {
709 		php_dom_throw_error(NOT_FOUND_ERR, dom_get_strict_error(context->document));
710 		return FAILURE;
711 	}
712 
713 	return SUCCESS;
714 }
715 
dom_child_node_remove(dom_object * context)716 void dom_child_node_remove(dom_object *context)
717 {
718 	xmlNode *child = dom_object_get_node(context);
719 
720 	if (UNEXPECTED(dom_child_removal_preconditions(child, context) != SUCCESS)) {
721 		return;
722 	}
723 
724 	php_libxml_invalidate_node_list_cache(context->document);
725 
726 	xmlUnlinkNode(child);
727 }
728 
729 /* https://dom.spec.whatwg.org/#dom-childnode-replacewith */
dom_child_replace_with(dom_object * context,zval * nodes,uint32_t nodesc)730 void dom_child_replace_with(dom_object *context, zval *nodes, uint32_t nodesc)
731 {
732 	if (UNEXPECTED(dom_sanity_check_node_list_types(nodes, nodesc, dom_get_node_ce(php_dom_follow_spec_doc_ref(context->document))) != SUCCESS)) {
733 		return;
734 	}
735 
736 	xmlNodePtr child = dom_object_get_node(context);
737 
738 	/* 1. Let parent be this’s parent. */
739 	xmlNodePtr parentNode = child->parent;
740 
741 	/* 2. If parent is null, then return. */
742 	if (UNEXPECTED(parentNode == NULL)) {
743 		return;
744 	}
745 
746 	/* 3. Let viableNextSibling be this’s first following sibling not in nodes; otherwise null. */
747 	xmlNodePtr viable_next_sibling = child->next;
748 	while (viable_next_sibling && dom_is_node_in_list(nodes, nodesc, viable_next_sibling)) {
749 		viable_next_sibling = viable_next_sibling->next;
750 	}
751 
752 	if (UNEXPECTED(dom_child_removal_preconditions(child, context) != SUCCESS)) {
753 		return;
754 	}
755 
756 	php_libxml_invalidate_node_list_cache(context->document);
757 
758 	/* 4. Let node be the result of converting nodes into a node, given nodes and this’s node document. */
759 	xmlNodePtr node = dom_zvals_to_single_node(context->document, parentNode, nodes, nodesc);
760 	if (UNEXPECTED(node == NULL)) {
761 		return;
762 	}
763 
764 	/* Spec step 5-6: perform the replacement */
765 	if (dom_is_pre_insert_valid_without_step_1(context->document, parentNode, node, viable_next_sibling, parentNode->doc)) {
766 		/* Unlink it unless it became a part of the fragment.
767 		 * Freeing will be taken care of by the lifetime of the returned dom object. */
768 		if (child->parent != node) {
769 			xmlUnlinkNode(child);
770 		}
771 
772 		dom_insert_node_list_unchecked(context->document, node, parentNode, viable_next_sibling);
773 	} else {
774 		dom_insert_node_list_cleanup(node);
775 	}
776 }
777 
778 /* https://dom.spec.whatwg.org/#dom-parentnode-replacechildren */
dom_parent_node_replace_children(dom_object * context,zval * nodes,uint32_t nodesc)779 void dom_parent_node_replace_children(dom_object *context, zval *nodes, uint32_t nodesc)
780 {
781 	if (UNEXPECTED(dom_sanity_check_node_list_types(nodes, nodesc, dom_get_node_ce(php_dom_follow_spec_doc_ref(context->document))) != SUCCESS)) {
782 		return;
783 	}
784 
785 	xmlNodePtr thisp = dom_object_get_node(context);
786 
787 	php_libxml_invalidate_node_list_cache(context->document);
788 
789 	/* 1. Let node be the result of converting nodes into a node given nodes and this’s node document. */
790 	xmlNodePtr node = dom_zvals_to_single_node(context->document, thisp, nodes, nodesc);
791 	if (UNEXPECTED(node == NULL)) {
792 		return;
793 	}
794 
795 	/* Spec steps 2-3: replace all */
796 	if (dom_is_pre_insert_valid_without_step_1(context->document, thisp, node, NULL, thisp->doc)) {
797 		dom_remove_all_children(thisp);
798 		php_dom_pre_insert(context->document, node, thisp, NULL);
799 	} else {
800 		dom_insert_node_list_cleanup(node);
801 	}
802 }
803 
804 #endif
805