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