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
dom_zvals_to_fragment(php_libxml_ref_obj * document,xmlNode * contextNode,zval * nodes,int nodesc)153 xmlNode* dom_zvals_to_fragment(php_libxml_ref_obj *document, xmlNode *contextNode, zval *nodes, int nodesc)
154 {
155 int i;
156 xmlDoc *documentNode;
157 xmlNode *fragment;
158 xmlNode *newNode;
159 dom_object *newNodeObj;
160
161 documentNode = dom_doc_from_context_node(contextNode);
162
163 fragment = xmlNewDocFragment(documentNode);
164
165 if (!fragment) {
166 return NULL;
167 }
168
169 for (i = 0; i < nodesc; i++) {
170 if (Z_TYPE(nodes[i]) == IS_OBJECT) {
171 newNodeObj = Z_DOMOBJ_P(&nodes[i]);
172 newNode = dom_object_get_node(newNodeObj);
173
174 if (newNode->parent != NULL) {
175 xmlUnlinkNode(newNode);
176 }
177
178 newNodeObj->document = document;
179 xmlSetTreeDoc(newNode, documentNode);
180
181 /* Citing from the docs (https://gnome.pages.gitlab.gnome.org/libxml2/devhelp/libxml2-tree.html#xmlAddChild):
182 * "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)".
183 * So we must take a copy if this situation arises to prevent a use-after-free. */
184 bool will_free = newNode->type == XML_TEXT_NODE && fragment->last && fragment->last->type == XML_TEXT_NODE;
185 if (will_free) {
186 newNode = xmlCopyNode(newNode, 1);
187 }
188
189 if (newNode->type == XML_DOCUMENT_FRAG_NODE) {
190 /* Unpack document fragment nodes, the behaviour differs for different libxml2 versions. */
191 newNode = newNode->children;
192 while (newNode) {
193 xmlNodePtr next = newNode->next;
194 xmlUnlinkNode(newNode);
195 if (!xmlAddChild(fragment, newNode)) {
196 goto err;
197 }
198 newNode = next;
199 }
200 } else if (!xmlAddChild(fragment, newNode)) {
201 if (will_free) {
202 xmlFreeNode(newNode);
203 }
204 goto err;
205 }
206 } else {
207 ZEND_ASSERT(Z_TYPE(nodes[i]) == IS_STRING);
208
209 newNode = xmlNewDocText(documentNode, (xmlChar *) Z_STRVAL(nodes[i]));
210
211 xmlSetTreeDoc(newNode, documentNode);
212
213 if (!xmlAddChild(fragment, newNode)) {
214 xmlFreeNode(newNode);
215 goto err;
216 }
217 }
218 }
219
220 return fragment;
221
222 err:
223 xmlFreeNode(fragment);
224 return NULL;
225 }
226
dom_fragment_assign_parent_node(xmlNodePtr parentNode,xmlNodePtr fragment)227 static void dom_fragment_assign_parent_node(xmlNodePtr parentNode, xmlNodePtr fragment)
228 {
229 xmlNodePtr node = fragment->children;
230
231 while (node != NULL) {
232 node->parent = parentNode;
233
234 if (node == fragment->last) {
235 break;
236 }
237 node = node->next;
238 }
239
240 fragment->children = NULL;
241 fragment->last = NULL;
242 }
243
dom_sanity_check_node_list_for_insertion(php_libxml_ref_obj * document,xmlNodePtr parentNode,zval * nodes,int nodesc)244 static zend_result dom_sanity_check_node_list_for_insertion(php_libxml_ref_obj *document, xmlNodePtr parentNode, zval *nodes, int nodesc)
245 {
246 if (document == NULL) {
247 php_dom_throw_error(HIERARCHY_REQUEST_ERR, 1);
248 return FAILURE;
249 }
250
251 xmlDocPtr documentNode = dom_doc_from_context_node(parentNode);
252
253 for (int i = 0; i < nodesc; i++) {
254 zend_uchar type = Z_TYPE(nodes[i]);
255 if (type == IS_OBJECT) {
256 const zend_class_entry *ce = Z_OBJCE(nodes[i]);
257
258 if (instanceof_function(ce, dom_node_class_entry)) {
259 xmlNodePtr node = dom_object_get_node(Z_DOMOBJ_P(nodes + i));
260
261 if (node->doc != documentNode) {
262 php_dom_throw_error(WRONG_DOCUMENT_ERR, dom_get_strict_error(document));
263 return FAILURE;
264 }
265
266 if (node->type == XML_ATTRIBUTE_NODE || dom_hierarchy(parentNode, node) != SUCCESS) {
267 php_dom_throw_error(HIERARCHY_REQUEST_ERR, dom_get_strict_error(document));
268 return FAILURE;
269 }
270 } else {
271 zend_argument_type_error(i + 1, "must be of type DOMNode|string, %s given", zend_zval_type_name(&nodes[i]));
272 return FAILURE;
273 }
274 } else if (type != IS_STRING) {
275 zend_argument_type_error(i + 1, "must be of type DOMNode|string, %s given", zend_zval_type_name(&nodes[i]));
276 return FAILURE;
277 }
278 }
279
280 return SUCCESS;
281 }
282
dom_pre_insert(xmlNodePtr insertion_point,xmlNodePtr parentNode,xmlNodePtr newchild,xmlNodePtr fragment)283 static void dom_pre_insert(xmlNodePtr insertion_point, xmlNodePtr parentNode, xmlNodePtr newchild, xmlNodePtr fragment)
284 {
285 if (!insertion_point) {
286 /* Place it as last node */
287 if (parentNode->children) {
288 /* There are children */
289 newchild->prev = parentNode->last;
290 parentNode->last->next = newchild;
291 } else {
292 /* No children, because they moved out when they became a fragment */
293 parentNode->children = newchild;
294 }
295 parentNode->last = fragment->last;
296 } else {
297 /* Insert fragment before insertion_point */
298 fragment->last->next = insertion_point;
299 if (insertion_point->prev) {
300 insertion_point->prev->next = newchild;
301 newchild->prev = insertion_point->prev;
302 }
303 insertion_point->prev = fragment->last;
304 if (parentNode->children == insertion_point) {
305 parentNode->children = newchild;
306 }
307 }
308 }
309
dom_parent_node_append(dom_object * context,zval * nodes,int nodesc)310 void dom_parent_node_append(dom_object *context, zval *nodes, int nodesc)
311 {
312 xmlNode *parentNode = dom_object_get_node(context);
313 xmlNodePtr newchild, prevsib;
314
315 if (UNEXPECTED(dom_sanity_check_node_list_for_insertion(context->document, parentNode, nodes, nodesc) != SUCCESS)) {
316 return;
317 }
318
319 xmlNode *fragment = dom_zvals_to_fragment(context->document, parentNode, nodes, nodesc);
320
321 if (fragment == NULL) {
322 return;
323 }
324
325 newchild = fragment->children;
326 prevsib = parentNode->last;
327
328 if (newchild) {
329 if (prevsib != NULL) {
330 prevsib->next = newchild;
331 } else {
332 parentNode->children = newchild;
333 }
334
335 xmlNodePtr last = fragment->last;
336 parentNode->last = last;
337
338 newchild->prev = prevsib;
339
340 dom_fragment_assign_parent_node(parentNode, fragment);
341
342 dom_reconcile_ns_list(parentNode->doc, newchild, last);
343 }
344
345 xmlFree(fragment);
346 }
347
dom_parent_node_prepend(dom_object * context,zval * nodes,int nodesc)348 void dom_parent_node_prepend(dom_object *context, zval *nodes, int nodesc)
349 {
350 xmlNode *parentNode = dom_object_get_node(context);
351
352 if (parentNode->children == NULL) {
353 dom_parent_node_append(context, nodes, nodesc);
354 return;
355 }
356
357 if (UNEXPECTED(dom_sanity_check_node_list_for_insertion(context->document, parentNode, nodes, nodesc) != SUCCESS)) {
358 return;
359 }
360
361 xmlNode *fragment = dom_zvals_to_fragment(context->document, parentNode, nodes, nodesc);
362
363 if (fragment == NULL) {
364 return;
365 }
366
367 xmlNode *newchild = fragment->children;
368
369 if (newchild) {
370 xmlNodePtr last = fragment->last;
371
372 dom_pre_insert(parentNode->children, parentNode, newchild, fragment);
373
374 dom_fragment_assign_parent_node(parentNode, fragment);
375
376 dom_reconcile_ns_list(parentNode->doc, newchild, last);
377 }
378
379 xmlFree(fragment);
380 }
381
dom_parent_node_after(dom_object * context,zval * nodes,int nodesc)382 void dom_parent_node_after(dom_object *context, zval *nodes, int nodesc)
383 {
384 /* Spec link: https://dom.spec.whatwg.org/#dom-childnode-after */
385
386 xmlNode *prevsib = dom_object_get_node(context);
387 xmlNodePtr newchild, parentNode;
388 xmlNode *fragment;
389 xmlDoc *doc;
390
391 /* Spec step 1 */
392 parentNode = prevsib->parent;
393 /* Spec step 2 */
394 if (!parentNode) {
395 int stricterror = dom_get_strict_error(context->document);
396 php_dom_throw_error(HIERARCHY_REQUEST_ERR, stricterror);
397 return;
398 }
399
400 /* Spec step 3: find first following child not in nodes; otherwise null */
401 xmlNodePtr viable_next_sibling = prevsib->next;
402 while (viable_next_sibling) {
403 if (!dom_is_node_in_list(nodes, nodesc, viable_next_sibling)) {
404 break;
405 }
406 viable_next_sibling = viable_next_sibling->next;
407 }
408
409 doc = prevsib->doc;
410
411 if (UNEXPECTED(dom_sanity_check_node_list_for_insertion(context->document, parentNode, nodes, nodesc) != SUCCESS)) {
412 return;
413 }
414
415 /* Spec step 4: convert nodes into fragment */
416 fragment = dom_zvals_to_fragment(context->document, parentNode, nodes, nodesc);
417
418 if (fragment == NULL) {
419 return;
420 }
421
422 newchild = fragment->children;
423
424 if (newchild) {
425 xmlNodePtr last = fragment->last;
426
427 /* Step 5: place fragment into the parent before viable_next_sibling */
428 dom_pre_insert(viable_next_sibling, parentNode, newchild, fragment);
429
430 dom_fragment_assign_parent_node(parentNode, fragment);
431 dom_reconcile_ns_list(doc, newchild, last);
432 }
433
434 xmlFree(fragment);
435 }
436
dom_parent_node_before(dom_object * context,zval * nodes,int nodesc)437 void dom_parent_node_before(dom_object *context, zval *nodes, int nodesc)
438 {
439 /* Spec link: https://dom.spec.whatwg.org/#dom-childnode-before */
440
441 xmlNode *nextsib = dom_object_get_node(context);
442 xmlNodePtr newchild, parentNode;
443 xmlNode *fragment;
444 xmlDoc *doc;
445
446 /* Spec step 1 */
447 parentNode = nextsib->parent;
448 /* Spec step 2 */
449 if (!parentNode) {
450 int stricterror = dom_get_strict_error(context->document);
451 php_dom_throw_error(HIERARCHY_REQUEST_ERR, stricterror);
452 return;
453 }
454
455 /* Spec step 3: find first following child not in nodes; otherwise null */
456 xmlNodePtr viable_previous_sibling = nextsib->prev;
457 while (viable_previous_sibling) {
458 if (!dom_is_node_in_list(nodes, nodesc, viable_previous_sibling)) {
459 break;
460 }
461 viable_previous_sibling = viable_previous_sibling->prev;
462 }
463
464 doc = nextsib->doc;
465
466 if (UNEXPECTED(dom_sanity_check_node_list_for_insertion(context->document, parentNode, nodes, nodesc) != SUCCESS)) {
467 return;
468 }
469
470 /* Spec step 4: convert nodes into fragment */
471 fragment = dom_zvals_to_fragment(context->document, parentNode, nodes, nodesc);
472
473 if (fragment == NULL) {
474 return;
475 }
476
477 newchild = fragment->children;
478
479 if (newchild) {
480 xmlNodePtr last = fragment->last;
481
482 /* Step 5: if viable_previous_sibling is null, set it to the parent's first child, otherwise viable_previous_sibling's next sibling */
483 if (!viable_previous_sibling) {
484 viable_previous_sibling = parentNode->children;
485 } else {
486 viable_previous_sibling = viable_previous_sibling->next;
487 }
488 /* Step 6: place fragment into the parent after viable_previous_sibling */
489 dom_pre_insert(viable_previous_sibling, parentNode, newchild, fragment);
490
491 dom_fragment_assign_parent_node(parentNode, fragment);
492 dom_reconcile_ns_list(doc, newchild, last);
493 }
494
495 xmlFree(fragment);
496 }
497
dom_child_removal_preconditions(const xmlNodePtr child,int stricterror)498 static zend_result dom_child_removal_preconditions(const xmlNodePtr child, int stricterror)
499 {
500 if (dom_node_is_read_only(child) == SUCCESS ||
501 (child->parent != NULL && dom_node_is_read_only(child->parent) == SUCCESS)) {
502 php_dom_throw_error(NO_MODIFICATION_ALLOWED_ERR, stricterror);
503 return FAILURE;
504 }
505
506 if (!child->parent) {
507 php_dom_throw_error(NOT_FOUND_ERR, stricterror);
508 return FAILURE;
509 }
510
511 if (dom_node_children_valid(child->parent) == FAILURE) {
512 return FAILURE;
513 }
514
515 xmlNodePtr children = child->parent->children;
516 if (!children) {
517 php_dom_throw_error(NOT_FOUND_ERR, stricterror);
518 return FAILURE;
519 }
520
521 return SUCCESS;
522 }
523
dom_child_node_remove(dom_object * context)524 void dom_child_node_remove(dom_object *context)
525 {
526 xmlNode *child = dom_object_get_node(context);
527 xmlNodePtr children;
528 int stricterror;
529
530 stricterror = dom_get_strict_error(context->document);
531
532 if (UNEXPECTED(dom_child_removal_preconditions(child, stricterror) != SUCCESS)) {
533 return;
534 }
535
536 children = child->parent->children;
537 while (children) {
538 if (children == child) {
539 xmlUnlinkNode(child);
540 return;
541 }
542 children = children->next;
543 }
544
545 php_dom_throw_error(NOT_FOUND_ERR, stricterror);
546 }
547
dom_child_replace_with(dom_object * context,zval * nodes,int nodesc)548 void dom_child_replace_with(dom_object *context, zval *nodes, int nodesc)
549 {
550 /* Spec link: https://dom.spec.whatwg.org/#dom-childnode-replacewith */
551
552 xmlNodePtr child = dom_object_get_node(context);
553
554 /* Spec step 1 */
555 xmlNodePtr parentNode = child->parent;
556 /* Spec step 2 */
557 if (!parentNode) {
558 int stricterror = dom_get_strict_error(context->document);
559 php_dom_throw_error(HIERARCHY_REQUEST_ERR, stricterror);
560 return;
561 }
562
563 int stricterror = dom_get_strict_error(context->document);
564 if (UNEXPECTED(dom_child_removal_preconditions(child, stricterror) != SUCCESS)) {
565 return;
566 }
567
568 /* Spec step 3: find first following child not in nodes; otherwise null */
569 xmlNodePtr viable_next_sibling = child->next;
570 while (viable_next_sibling) {
571 if (!dom_is_node_in_list(nodes, nodesc, viable_next_sibling)) {
572 break;
573 }
574 viable_next_sibling = viable_next_sibling->next;
575 }
576
577 if (UNEXPECTED(dom_sanity_check_node_list_for_insertion(context->document, parentNode, nodes, nodesc) != SUCCESS)) {
578 return;
579 }
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 xmlDocPtr doc = parentNode->doc;
591
592 /* Unlink and free it unless it became a part of the fragment. */
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
609 #endif
610