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