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 * => Impossible */
243 ZEND_ASSERT(!php_dom_pre_insert_is_parent_invalid(parentNode));
244
245 if (node->doc != documentNode) {
246 php_dom_throw_error(WRONG_DOCUMENT_ERR, dom_get_strict_error(document));
247 return false;
248 }
249
250 /* 3. If child is non-null and its parent is not parent, then throw a "NotFoundError" DOMException. */
251 if (child != NULL && child->parent != parentNode) {
252 php_dom_throw_error(NOT_FOUND_ERR, dom_get_strict_error(document));
253 return false;
254 }
255
256 bool parent_is_document = parentNode->type == XML_DOCUMENT_NODE || parentNode->type == XML_HTML_DOCUMENT_NODE;
257
258 if (/* 2. If node is a host-including inclusive ancestor of parent, then throw a "HierarchyRequestError" DOMException. */
259 dom_hierarchy(parentNode, node) != SUCCESS
260 /* 4. If node is not a DocumentFragment, DocumentType, Element, or CharacterData node, then throw a "HierarchyRequestError" DOMException. */
261 || node->type == XML_ATTRIBUTE_NODE
262 || (php_dom_follow_spec_doc_ref(document) && (
263 node->type == XML_ENTITY_REF_NODE
264 || node->type == XML_ENTITY_NODE
265 || node->type == XML_NOTATION_NODE
266 || node->type == XML_DOCUMENT_NODE
267 || node->type == XML_HTML_DOCUMENT_NODE
268 || node->type >= XML_ELEMENT_DECL))) {
269 php_dom_throw_error(HIERARCHY_REQUEST_ERR, dom_get_strict_error(document));
270 return false;
271 }
272
273 if (php_dom_follow_spec_doc_ref(document)) {
274 /* 5. If either node is a Text node and parent is a document... */
275 if (parent_is_document && (node->type == XML_TEXT_NODE || node->type == XML_CDATA_SECTION_NODE)) {
276 php_dom_throw_error_with_message(HIERARCHY_REQUEST_ERR, "Cannot insert text as a child of a document", /* strict */ true);
277 return false;
278 }
279
280 /* 5. ..., or node is a doctype and parent is not a document, then throw a "HierarchyRequestError" DOMException. */
281 if (!parent_is_document && node->type == XML_DTD_NODE) {
282 php_dom_throw_error_with_message(HIERARCHY_REQUEST_ERR, "Cannot insert a document type into anything other than a document", /* strict */ true);
283 return false;
284 }
285
286 /* 6. If parent is a document, and any of the statements below, switched on the interface node implements,
287 * are true, then throw a "HierarchyRequestError" DOMException. */
288 if (parent_is_document) {
289 /* DocumentFragment */
290 if (node->type == XML_DOCUMENT_FRAG_NODE) {
291 if (!php_dom_fragment_insertion_hierarchy_check_pre_insertion(parentNode, node, child)) {
292 return false;
293 }
294 }
295 /* Element */
296 else if (node->type == XML_ELEMENT_NODE) {
297 /* parent has an element child, child is a doctype, or child is non-null and a doctype is following child. */
298 if (php_dom_has_child_of_type(parentNode, XML_ELEMENT_NODE)) {
299 php_dom_throw_error_with_message(HIERARCHY_REQUEST_ERR, "Cannot have more than one element child in a document", /* strict */ true);
300 return false;
301 }
302 if (child != NULL && (child->type == XML_DTD_NODE || php_dom_has_sibling_following_node(child, XML_DTD_NODE))) {
303 php_dom_throw_error_with_message(HIERARCHY_REQUEST_ERR, "Document types must be the first child in a document", /* strict */ true);
304 return false;
305 }
306 }
307 /* DocumentType */
308 else if (node->type == XML_DTD_NODE) {
309 /* 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. */
310 if (php_dom_has_child_of_type(parentNode, XML_DTD_NODE)) {
311 php_dom_throw_error_with_message(HIERARCHY_REQUEST_ERR, "Cannot have more than one document type", /* strict */ true);
312 return false;
313 }
314 if ((child != NULL && php_dom_has_sibling_preceding_node(child, XML_ELEMENT_NODE))
315 || (child == NULL && php_dom_has_child_of_type(parentNode, XML_ELEMENT_NODE))) {
316 php_dom_throw_error_with_message(HIERARCHY_REQUEST_ERR, "Document types must be the first child in a document", /* strict */ true);
317 return false;
318 }
319 }
320 }
321 }
322
323 return true;
324 }
325
dom_free_node_after_zval_single_node_creation(xmlNodePtr node)326 static void dom_free_node_after_zval_single_node_creation(xmlNodePtr node)
327 {
328 /* For the object cases, the user did provide them, so they don't have to be freed as there are still references.
329 * For the newly created text nodes, we do have to free them. */
330 xmlNodePtr next;
331 for (xmlNodePtr child = node->children; child != NULL; child = next) {
332 next = child->next;
333 xmlUnlinkNode(child);
334 if (child->_private == NULL) {
335 xmlFreeNode(child);
336 }
337 }
338 }
339
340 /* 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)341 xmlNode* dom_zvals_to_single_node(php_libxml_ref_obj *document, xmlNode *contextNode, zval *nodes, uint32_t nodesc)
342 {
343 xmlDoc *documentNode;
344 xmlNode *newNode;
345 dom_object *newNodeObj;
346
347 documentNode = dom_doc_from_context_node(contextNode);
348
349 /* 1. Let node be null. */
350 xmlNodePtr node = NULL;
351
352 /* 2. => handled in the loop. */
353
354 /* 3. If nodes contains one node, then set node to nodes[0]. */
355 if (nodesc == 1) {
356 /* ... and return */
357 if (Z_TYPE_P(nodes) == IS_OBJECT) {
358 return dom_object_get_node(Z_DOMOBJ_P(nodes));
359 } else {
360 ZEND_ASSERT(Z_TYPE_P(nodes) == IS_STRING);
361 node = xmlNewDocTextLen(documentNode, BAD_CAST Z_STRVAL_P(nodes), Z_STRLEN_P(nodes));
362 if (UNEXPECTED(node == NULL)) {
363 dom_cannot_create_temp_nodes();
364 }
365 return node;
366 }
367 }
368
369 node = xmlNewDocFragment(documentNode);
370 if (UNEXPECTED(!node)) {
371 dom_cannot_create_temp_nodes();
372 return NULL;
373 }
374
375 /* 4. Otherwise, set node to a new DocumentFragment node whose node document is document,
376 * and then append each node in nodes, if any, to it. */
377 for (uint32_t i = 0; i < nodesc; i++) {
378 if (Z_TYPE(nodes[i]) == IS_OBJECT) {
379 newNodeObj = Z_DOMOBJ_P(&nodes[i]);
380 newNode = dom_object_get_node(newNodeObj);
381
382 if (UNEXPECTED(!newNode)) {
383 php_dom_throw_error(INVALID_STATE_ERR, /* strict */ true);
384 goto err;
385 }
386
387 if (!dom_is_pre_insert_valid_without_step_1(document, node, newNode, NULL, documentNode)) {
388 goto err;
389 }
390
391 if (newNode->parent != NULL) {
392 xmlUnlinkNode(newNode);
393 }
394
395 ZEND_ASSERT(newNodeObj->document == document);
396
397 if (newNode->type == XML_DOCUMENT_FRAG_NODE) {
398 /* Unpack document fragment nodes, the behaviour differs for different libxml2 versions. */
399 newNode = newNode->children;
400 while (newNode) {
401 xmlNodePtr next = newNode->next;
402 xmlUnlinkNode(newNode);
403 dom_add_child_without_merging(node, newNode);
404 newNode = next;
405 }
406 } else {
407 dom_add_child_without_merging(node, newNode);
408 }
409 } else {
410 /* 2. Replace each string in nodes with a new Text node whose data is the string and node document is document. */
411 ZEND_ASSERT(Z_TYPE(nodes[i]) == IS_STRING);
412
413 /* Text nodes can't violate the hierarchy at this point. */
414 newNode = xmlNewDocTextLen(documentNode, BAD_CAST Z_STRVAL(nodes[i]), Z_STRLEN(nodes[i]));
415 if (UNEXPECTED(newNode == NULL)) {
416 dom_cannot_create_temp_nodes();
417 goto err;
418 }
419 dom_add_child_without_merging(node, newNode);
420 }
421 }
422
423 /* 5. Return node. */
424 return node;
425
426 err:
427 /* For the object cases, the user did provide them, so they don't have to be freed as there are still references.
428 * For the newly created text nodes, we do have to free them. */
429 dom_free_node_after_zval_single_node_creation(node);
430 xmlFree(node);
431 return NULL;
432 }
433
dom_sanity_check_node_list_types(zval * nodes,uint32_t nodesc,zend_class_entry * node_ce)434 static zend_result dom_sanity_check_node_list_types(zval *nodes, uint32_t nodesc, zend_class_entry *node_ce)
435 {
436 for (uint32_t i = 0; i < nodesc; i++) {
437 zend_uchar type = Z_TYPE(nodes[i]);
438 if (type == IS_OBJECT) {
439 const zend_class_entry *ce = Z_OBJCE(nodes[i]);
440
441 if (!instanceof_function(ce, node_ce)) {
442 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]));
443 return FAILURE;
444 }
445 } else if (type == IS_STRING) {
446 if (Z_STRLEN(nodes[i]) > INT_MAX) {
447 zend_argument_value_error(i + 1, "must be less than or equal to %d bytes long", INT_MAX);
448 return FAILURE;
449 }
450 } else {
451 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]));
452 return FAILURE;
453 }
454 }
455
456 return SUCCESS;
457 }
458
php_dom_pre_insert_helper(xmlNodePtr insertion_point,xmlNodePtr parentNode,xmlNodePtr newchild,xmlNodePtr last)459 static void php_dom_pre_insert_helper(xmlNodePtr insertion_point, xmlNodePtr parentNode, xmlNodePtr newchild, xmlNodePtr last)
460 {
461 if (!insertion_point) {
462 /* Place it as last node */
463 if (parentNode->children) {
464 /* There are children */
465 newchild->prev = parentNode->last;
466 parentNode->last->next = newchild;
467 } else {
468 /* No children, because they moved out when they became a fragment */
469 parentNode->children = newchild;
470 }
471 parentNode->last = last;
472 } else {
473 /* Insert fragment before insertion_point */
474 last->next = insertion_point;
475 if (insertion_point->prev) {
476 insertion_point->prev->next = newchild;
477 newchild->prev = insertion_point->prev;
478 }
479 insertion_point->prev = last;
480 if (parentNode->children == insertion_point) {
481 parentNode->children = newchild;
482 }
483 }
484 }
485
dom_insert_node_list_cleanup(xmlNodePtr node)486 static void dom_insert_node_list_cleanup(xmlNodePtr node)
487 {
488 if (node->_private != NULL) {
489 /* Not a temporary node. */
490 return;
491 }
492 if (node->type == XML_DOCUMENT_FRAG_NODE) {
493 dom_free_node_after_zval_single_node_creation(node);
494 xmlFree(node); /* Don't free the children, now-empty fragment! */
495 } else if (node->type == XML_TEXT_NODE) {
496 ZEND_ASSERT(node->parent == NULL);
497 xmlFreeNode(node);
498 } else {
499 /* Must have been a directly-passed node. */
500 ZEND_ASSERT(node->_private != NULL);
501 }
502 }
503
504 /* 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)505 static void dom_insert_node_list_unchecked(php_libxml_ref_obj *document, xmlNodePtr node, xmlNodePtr parent, xmlNodePtr insertion_point)
506 {
507 /* Step 1 should be checked by the caller. */
508
509 if (node->type == XML_DOCUMENT_FRAG_NODE) {
510 /* Steps 2-3 are not applicable here, the condition is impossible. */
511
512 xmlNodePtr newchild = node->children;
513
514 /* 4. Insert node into parent before referenceChild (i.e. insertion_point here because of the impossible condition). */
515 if (newchild) {
516 xmlNodePtr last = node->last;
517 php_dom_pre_insert_helper(insertion_point, parent, newchild, last);
518 dom_fragment_assign_parent_node(parent, node);
519 if (!php_dom_follow_spec_doc_ref(document)) {
520 dom_reconcile_ns_list(parent->doc, newchild, last);
521 }
522 if (parent->doc && newchild->type == XML_DTD_NODE) {
523 parent->doc->intSubset = (xmlDtdPtr) newchild;
524 newchild->parent = (xmlNodePtr) parent->doc;
525 }
526 }
527
528 if (node->_private == NULL) {
529 xmlFree(node);
530 } else {
531 node->children = NULL;
532 node->last = NULL;
533 }
534 } else {
535 /* 2. Let referenceChild be child.
536 * 3. If referenceChild is node, then set referenceChild to node’s next sibling. */
537 if (insertion_point == node) {
538 insertion_point = node->next;
539 }
540
541 /* 4. Insert node into parent before referenceChild. */
542 xmlUnlinkNode(node);
543 php_dom_pre_insert_helper(insertion_point, parent, node, node);
544 node->parent = parent;
545 if (parent->doc && node->type == XML_DTD_NODE) {
546 parent->doc->intSubset = (xmlDtdPtr) node;
547 node->parent = (xmlNodePtr) parent->doc;
548 } else {
549 if (!php_dom_follow_spec_doc_ref(document)) {
550 dom_reconcile_ns(parent->doc, node);
551 }
552 }
553 }
554 }
555
556 /* 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)557 bool php_dom_pre_insert(php_libxml_ref_obj *document, xmlNodePtr node, xmlNodePtr parent, xmlNodePtr insertion_point)
558 {
559 if (UNEXPECTED(node == NULL)) {
560 return false;
561 }
562
563 /* Step 1 checked here, other steps delegated to other function. */
564 if (dom_is_pre_insert_valid_without_step_1(document, parent, node, insertion_point, parent->doc)) {
565 dom_insert_node_list_unchecked(document, node, parent, insertion_point);
566 return true;
567 } else {
568 dom_insert_node_list_cleanup(node);
569 return false;
570 }
571 }
572
573 /* https://dom.spec.whatwg.org/#concept-node-append */
php_dom_node_append(php_libxml_ref_obj * document,xmlNodePtr node,xmlNodePtr parent)574 void php_dom_node_append(php_libxml_ref_obj *document, xmlNodePtr node, xmlNodePtr parent)
575 {
576 php_dom_pre_insert(document, node, parent, NULL);
577 }
578
579 /* https://dom.spec.whatwg.org/#dom-parentnode-append */
dom_parent_node_append(dom_object * context,zval * nodes,uint32_t nodesc)580 void dom_parent_node_append(dom_object *context, zval *nodes, uint32_t nodesc)
581 {
582 if (UNEXPECTED(dom_sanity_check_node_list_types(nodes, nodesc, dom_get_node_ce(php_dom_follow_spec_doc_ref(context->document))) != SUCCESS)) {
583 return;
584 }
585
586 xmlNode *parentNode = dom_object_get_node(context);
587
588 php_libxml_invalidate_node_list_cache(context->document);
589
590 /* 1. Let node be the result of converting nodes into a node given nodes and this’s node document. */
591 xmlNodePtr node = dom_zvals_to_single_node(context->document, parentNode, nodes, nodesc);
592 if (UNEXPECTED(node == NULL)) {
593 return;
594 }
595
596 /* 2. Append node to this. */
597 php_dom_node_append(context->document, node, parentNode);
598 }
599
600 /* https://dom.spec.whatwg.org/#dom-parentnode-prepend */
dom_parent_node_prepend(dom_object * context,zval * nodes,uint32_t nodesc)601 void dom_parent_node_prepend(dom_object *context, zval *nodes, uint32_t nodesc)
602 {
603 xmlNode *parentNode = dom_object_get_node(context);
604
605 if (parentNode->children == NULL) {
606 dom_parent_node_append(context, nodes, nodesc);
607 return;
608 }
609
610 if (UNEXPECTED(dom_sanity_check_node_list_types(nodes, nodesc, dom_get_node_ce(php_dom_follow_spec_doc_ref(context->document))) != SUCCESS)) {
611 return;
612 }
613
614 php_libxml_invalidate_node_list_cache(context->document);
615
616 /* 1. Let node be the result of converting nodes into a node given nodes and this’s node document. */
617 xmlNodePtr node = dom_zvals_to_single_node(context->document, parentNode, nodes, nodesc);
618 if (UNEXPECTED(node == NULL)) {
619 return;
620 }
621
622 /* 2. Pre-insert node into this before this’s first child. */
623 php_dom_pre_insert(context->document, node, parentNode, parentNode->children);
624 }
625
626 /* https://dom.spec.whatwg.org/#dom-childnode-after */
dom_parent_node_after(dom_object * context,zval * nodes,uint32_t nodesc)627 void dom_parent_node_after(dom_object *context, zval *nodes, uint32_t nodesc)
628 {
629 if (UNEXPECTED(dom_sanity_check_node_list_types(nodes, nodesc, dom_get_node_ce(php_dom_follow_spec_doc_ref(context->document))) != SUCCESS)) {
630 return;
631 }
632
633 xmlNode *thisp = dom_object_get_node(context);
634
635 /* 1. Let parent be this’s parent. */
636 xmlNodePtr parentNode = thisp->parent;
637
638 /* 2. If parent is null, then return. */
639 if (UNEXPECTED(parentNode == NULL)) {
640 return;
641 }
642
643 /* 3. Let viableNextSibling be this’s first following sibling not in nodes; otherwise null. */
644 xmlNodePtr viable_next_sibling = thisp->next;
645 while (viable_next_sibling && dom_is_node_in_list(nodes, nodesc, viable_next_sibling)) {
646 viable_next_sibling = viable_next_sibling->next;
647 }
648
649 php_libxml_invalidate_node_list_cache(context->document);
650
651 /* 4. Let node be the result of converting nodes into a node, given nodes and this’s node document. */
652 xmlNodePtr fragment = dom_zvals_to_single_node(context->document, parentNode, nodes, nodesc);
653
654 /* 5. Pre-insert node into parent before viableNextSibling. */
655 php_dom_pre_insert(context->document, fragment, parentNode, viable_next_sibling);
656 }
657
658 /* https://dom.spec.whatwg.org/#dom-childnode-before */
dom_parent_node_before(dom_object * context,zval * nodes,uint32_t nodesc)659 void dom_parent_node_before(dom_object *context, zval *nodes, uint32_t nodesc)
660 {
661 if (UNEXPECTED(dom_sanity_check_node_list_types(nodes, nodesc, dom_get_node_ce(php_dom_follow_spec_doc_ref(context->document))) != SUCCESS)) {
662 return;
663 }
664
665 xmlNode *thisp = dom_object_get_node(context);
666
667 /* 1. Let parent be this’s parent. */
668 xmlNodePtr parentNode = thisp->parent;
669
670 /* 2. If parent is null, then return. */
671 if (UNEXPECTED(parentNode == NULL)) {
672 return;
673 }
674
675 /* 3. Let viablePreviousSibling be this’s first preceding sibling not in nodes; otherwise null. */
676 xmlNodePtr viable_previous_sibling = thisp->prev;
677 while (viable_previous_sibling && dom_is_node_in_list(nodes, nodesc, viable_previous_sibling)) {
678 viable_previous_sibling = viable_previous_sibling->prev;
679 }
680
681 php_libxml_invalidate_node_list_cache(context->document);
682
683 /* 4. Let node be the result of converting nodes into a node, given nodes and this’s node document. */
684 xmlNodePtr fragment = dom_zvals_to_single_node(context->document, parentNode, nodes, nodesc);
685
686 /* 5. If viable_previous_sibling is null, set it to the parent's first child, otherwise viable_previous_sibling's next sibling. */
687 if (!viable_previous_sibling) {
688 viable_previous_sibling = parentNode->children;
689 } else {
690 viable_previous_sibling = viable_previous_sibling->next;
691 }
692
693 /* 6. Pre-insert node into parent before viablePreviousSibling. */
694 php_dom_pre_insert(context->document, fragment, parentNode, viable_previous_sibling);
695 }
696
dom_child_removal_preconditions(const xmlNode * child,const dom_object * context)697 static zend_result dom_child_removal_preconditions(const xmlNode *child, const dom_object *context)
698 {
699 if (dom_node_is_read_only(child) == SUCCESS ||
700 (child->parent != NULL && dom_node_is_read_only(child->parent) == SUCCESS)) {
701 php_dom_throw_error(NO_MODIFICATION_ALLOWED_ERR, dom_get_strict_error(context->document));
702 return FAILURE;
703 }
704
705 if (!child->parent) {
706 php_dom_throw_error(NOT_FOUND_ERR, dom_get_strict_error(context->document));
707 return FAILURE;
708 }
709
710 return SUCCESS;
711 }
712
dom_child_node_remove(dom_object * context)713 void dom_child_node_remove(dom_object *context)
714 {
715 xmlNode *child = dom_object_get_node(context);
716
717 if (UNEXPECTED(dom_child_removal_preconditions(child, context) != SUCCESS)) {
718 return;
719 }
720
721 php_libxml_invalidate_node_list_cache(context->document);
722
723 xmlUnlinkNode(child);
724 }
725
726 /* https://dom.spec.whatwg.org/#dom-childnode-replacewith */
dom_child_replace_with(dom_object * context,zval * nodes,uint32_t nodesc)727 void dom_child_replace_with(dom_object *context, zval *nodes, uint32_t nodesc)
728 {
729 if (UNEXPECTED(dom_sanity_check_node_list_types(nodes, nodesc, dom_get_node_ce(php_dom_follow_spec_doc_ref(context->document))) != SUCCESS)) {
730 return;
731 }
732
733 xmlNodePtr child = dom_object_get_node(context);
734
735 /* 1. Let parent be this’s parent. */
736 xmlNodePtr parentNode = child->parent;
737
738 /* 2. If parent is null, then return. */
739 if (UNEXPECTED(parentNode == NULL)) {
740 return;
741 }
742
743 /* 3. Let viableNextSibling be this’s first following sibling not in nodes; otherwise null. */
744 xmlNodePtr viable_next_sibling = child->next;
745 while (viable_next_sibling && dom_is_node_in_list(nodes, nodesc, viable_next_sibling)) {
746 viable_next_sibling = viable_next_sibling->next;
747 }
748
749 if (UNEXPECTED(dom_child_removal_preconditions(child, context) != SUCCESS)) {
750 return;
751 }
752
753 php_libxml_invalidate_node_list_cache(context->document);
754
755 /* 4. Let node be the result of converting nodes into a node, given nodes and this’s node document. */
756 xmlNodePtr node = dom_zvals_to_single_node(context->document, parentNode, nodes, nodesc);
757 if (UNEXPECTED(node == NULL)) {
758 return;
759 }
760
761 /* Spec step 5-6: perform the replacement */
762 if (dom_is_pre_insert_valid_without_step_1(context->document, parentNode, node, viable_next_sibling, parentNode->doc)) {
763 /* Unlink it unless it became a part of the fragment.
764 * Freeing will be taken care of by the lifetime of the returned dom object. */
765 if (child->parent != node) {
766 xmlUnlinkNode(child);
767 }
768
769 dom_insert_node_list_unchecked(context->document, node, parentNode, viable_next_sibling);
770 } else {
771 dom_insert_node_list_cleanup(node);
772 }
773 }
774
775 /* https://dom.spec.whatwg.org/#dom-parentnode-replacechildren */
dom_parent_node_replace_children(dom_object * context,zval * nodes,uint32_t nodesc)776 void dom_parent_node_replace_children(dom_object *context, zval *nodes, uint32_t nodesc)
777 {
778 if (UNEXPECTED(dom_sanity_check_node_list_types(nodes, nodesc, dom_get_node_ce(php_dom_follow_spec_doc_ref(context->document))) != SUCCESS)) {
779 return;
780 }
781
782 xmlNodePtr thisp = dom_object_get_node(context);
783
784 php_libxml_invalidate_node_list_cache(context->document);
785
786 /* 1. Let node be the result of converting nodes into a node given nodes and this’s node document. */
787 xmlNodePtr node = dom_zvals_to_single_node(context->document, thisp, nodes, nodesc);
788 if (UNEXPECTED(node == NULL)) {
789 return;
790 }
791
792 /* Spec steps 2-3: replace all */
793 if (dom_is_pre_insert_valid_without_step_1(context->document, thisp, node, NULL, thisp->doc)) {
794 dom_remove_all_children(thisp);
795 php_dom_pre_insert(context->document, node, thisp, NULL);
796 } else {
797 dom_insert_node_list_cleanup(node);
798 }
799 }
800
801 #endif
802