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