@@ -124,6 +124,23 @@ int dom_parent_node_child_element_count(dom_object *obj, zval *retval)
124124}
125125/* }}} */
126126
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+
127144xmlNode * dom_zvals_to_fragment (php_libxml_ref_obj * document , xmlNode * contextNode , zval * nodes , int nodesc )
128145{
129146 int i ;
@@ -177,17 +194,16 @@ xmlNode* dom_zvals_to_fragment(php_libxml_ref_obj *document, xmlNode *contextNod
177194 goto hierarchy_request_err ;
178195 }
179196
180- /*
181- * xmlNewDocText function will always returns same address to the second parameter if the parameters are greater than or equal to three.
182- * If it's text, that's fine, but if it's an object, it can cause invalid pointer because many new nodes point to the same memory address.
183- * So we must copy the new node to avoid this situation.
184- */
185- if (nodesc > 1 ) {
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 ) {
186202 newNode = xmlCopyNode (newNode , 1 );
187203 }
188204
189205 if (!xmlAddChild (fragment , newNode )) {
190- if (nodesc > 1 ) {
206+ if (will_free ) {
191207 xmlFreeNode (newNode );
192208 }
193209 goto hierarchy_request_err ;
@@ -303,25 +319,64 @@ void dom_parent_node_prepend(dom_object *context, zval *nodes, int nodesc)
303319 xmlFree (fragment );
304320}
305321
322+ static void dom_pre_insert (xmlNodePtr insertion_point , xmlNodePtr parentNode , xmlNodePtr newchild , xmlNodePtr fragment )
323+ {
324+ if (!insertion_point ) {
325+ /* Place it as last node */
326+ if (parentNode -> children ) {
327+ /* There are children */
328+ fragment -> last -> prev = parentNode -> last ;
329+ newchild -> prev = parentNode -> last -> prev ;
330+ parentNode -> last -> next = newchild ;
331+ } else {
332+ /* No children, because they moved out when they became a fragment */
333+ parentNode -> children = newchild ;
334+ parentNode -> last = newchild ;
335+ }
336+ } else {
337+ /* Insert fragment before insertion_point */
338+ fragment -> last -> next = insertion_point ;
339+ if (insertion_point -> prev ) {
340+ insertion_point -> prev -> next = newchild ;
341+ newchild -> prev = insertion_point -> prev ;
342+ }
343+ insertion_point -> prev = newchild ;
344+ if (parentNode -> children == insertion_point ) {
345+ parentNode -> children = newchild ;
346+ }
347+ }
348+ }
349+
306350void dom_parent_node_after (dom_object * context , zval * nodes , int nodesc )
307351{
352+ /* Spec link: https://dom.spec.whatwg.org/#dom-childnode-after */
353+
308354 xmlNode * prevsib = dom_object_get_node (context );
309355 xmlNodePtr newchild , parentNode ;
310- xmlNode * fragment , * nextsib ;
356+ xmlNode * fragment ;
311357 xmlDoc * doc ;
312- bool afterlastchild ;
313-
314- int stricterror = dom_get_strict_error (context -> document );
315358
316- if (!prevsib -> parent ) {
317- php_dom_throw_error (NO_MODIFICATION_ALLOWED_ERR , stricterror );
359+ /* Spec step 1 */
360+ parentNode = prevsib -> parent ;
361+ /* Spec step 2 */
362+ if (!parentNode ) {
363+ int stricterror = dom_get_strict_error (context -> document );
364+ php_dom_throw_error (HIERARCHY_REQUEST_ERR , stricterror );
318365 return ;
319366 }
320367
368+ /* Spec step 3: find first following child not in nodes; otherwise null */
369+ xmlNodePtr viable_next_sibling = prevsib -> next ;
370+ while (viable_next_sibling ) {
371+ if (!dom_is_node_in_list (nodes , nodesc , viable_next_sibling )) {
372+ break ;
373+ }
374+ viable_next_sibling = viable_next_sibling -> next ;
375+ }
376+
321377 doc = prevsib -> doc ;
322- parentNode = prevsib -> parent ;
323- nextsib = prevsib -> next ;
324- afterlastchild = (nextsib == NULL );
378+
379+ /* Spec step 4: convert nodes into fragment */
325380 fragment = dom_zvals_to_fragment (context -> document , parentNode , nodes , nodesc );
326381
327382 if (fragment == NULL ) {
@@ -331,40 +386,9 @@ void dom_parent_node_after(dom_object *context, zval *nodes, int nodesc)
331386 newchild = fragment -> children ;
332387
333388 if (newchild ) {
334- /* first node and last node are both both parameters to DOMElement::after() method so nextsib and prevsib are null. */
335- if (!parentNode -> children ) {
336- prevsib = nextsib = NULL ;
337- } else if (afterlastchild ) {
338- /*
339- * The new node will be inserted after last node, prevsib is last node.
340- * The first node is the parameter to DOMElement::after() if parentNode->children == prevsib is true
341- * and prevsib does not change, otherwise prevsib is parentNode->last (first node).
342- */
343- prevsib = parentNode -> children == prevsib ? prevsib : parentNode -> last ;
344- } else {
345- /*
346- * The new node will be inserted after first node, prevsib is first node.
347- * The first node is not the parameter to DOMElement::after() if parentNode->children == prevsib is true
348- * and prevsib does not change otherwise prevsib is null to mean that parentNode->children is the new node.
349- */
350- prevsib = parentNode -> children == prevsib ? prevsib : NULL ;
351- }
352-
353- if (prevsib ) {
354- fragment -> last -> next = prevsib -> next ;
355- if (prevsib -> next ) {
356- prevsib -> next -> prev = fragment -> last ;
357- }
358- prevsib -> next = newchild ;
359- } else {
360- parentNode -> children = newchild ;
361- if (nextsib ) {
362- fragment -> last -> next = nextsib ;
363- nextsib -> prev = fragment -> last ;
364- }
365- }
389+ /* Step 5: place fragment into the parent before viable_next_sibling */
390+ dom_pre_insert (viable_next_sibling , parentNode , newchild , fragment );
366391
367- newchild -> prev = prevsib ;
368392 dom_fragment_assign_parent_node (parentNode , fragment );
369393 dom_reconcile_ns (doc , newchild );
370394 }
@@ -374,17 +398,34 @@ void dom_parent_node_after(dom_object *context, zval *nodes, int nodesc)
374398
375399void dom_parent_node_before (dom_object * context , zval * nodes , int nodesc )
376400{
401+ /* Spec link: https://dom.spec.whatwg.org/#dom-childnode-before */
402+
377403 xmlNode * nextsib = dom_object_get_node (context );
378- xmlNodePtr newchild , prevsib , parentNode ;
379- xmlNode * fragment , * afternextsib ;
404+ xmlNodePtr newchild , parentNode ;
405+ xmlNode * fragment ;
380406 xmlDoc * doc ;
381- bool beforefirstchild ;
382407
383- doc = nextsib -> doc ;
384- prevsib = nextsib -> prev ;
385- afternextsib = nextsib -> next ;
408+ /* Spec step 1 */
386409 parentNode = nextsib -> parent ;
387- beforefirstchild = !prevsib ;
410+ /* Spec step 2 */
411+ if (!parentNode ) {
412+ int stricterror = dom_get_strict_error (context -> document );
413+ php_dom_throw_error (HIERARCHY_REQUEST_ERR , stricterror );
414+ return ;
415+ }
416+
417+ /* Spec step 3: find first following child not in nodes; otherwise null */
418+ xmlNodePtr viable_previous_sibling = nextsib -> prev ;
419+ while (viable_previous_sibling ) {
420+ if (!dom_is_node_in_list (nodes , nodesc , viable_previous_sibling )) {
421+ break ;
422+ }
423+ viable_previous_sibling = viable_previous_sibling -> prev ;
424+ }
425+
426+ doc = nextsib -> doc ;
427+
428+ /* Spec step 4: convert nodes into fragment */
388429 fragment = dom_zvals_to_fragment (context -> document , parentNode , nodes , nodesc );
389430
390431 if (fragment == NULL ) {
@@ -394,37 +435,14 @@ void dom_parent_node_before(dom_object *context, zval *nodes, int nodesc)
394435 newchild = fragment -> children ;
395436
396437 if (newchild ) {
397- /* first node and last node are both both parameters to DOMElement::before() method so nextsib is null. */
398- if (!parentNode -> children ) {
399- nextsib = NULL ;
400- } else if (beforefirstchild ) {
401- /*
402- * The new node will be inserted before first node, nextsib is first node and afternextsib is last node.
403- * The first node is not the parameter to DOMElement::before() if parentNode->children == nextsib is true
404- * and nextsib does not change, otherwise nextsib is the last node.
405- */
406- nextsib = parentNode -> children == nextsib ? nextsib : afternextsib ;
407- } else {
408- /*
409- * The new node will be inserted before last node, prevsib is first node and nestsib is last node.
410- * The first node is not the parameter to DOMElement::before() if parentNode->children == prevsib is true
411- * but last node may be, so use prevsib->next to determine the value of nextsib, otherwise nextsib does not change.
412- */
413- nextsib = parentNode -> children == prevsib ? prevsib -> next : nextsib ;
414- }
415-
416- if (parentNode -> children == nextsib ) {
417- parentNode -> children = newchild ;
438+ /* Step 5: if viable_previous_sibling is null, set it to the parent's first child, otherwise viable_previous_sibling's next sibling */
439+ if (!viable_previous_sibling ) {
440+ viable_previous_sibling = parentNode -> children ;
418441 } else {
419- prevsib -> next = newchild ;
420- }
421-
422- fragment -> last -> next = nextsib ;
423- if (nextsib ) {
424- nextsib -> prev = fragment -> last ;
442+ viable_previous_sibling = viable_previous_sibling -> next ;
425443 }
426-
427- newchild -> prev = prevsib ;
444+ /* Step 6: place fragment into the parent after viable_previous_sibling */
445+ dom_pre_insert ( viable_previous_sibling , parentNode , newchild , fragment ) ;
428446
429447 dom_fragment_assign_parent_node (parentNode , fragment );
430448 dom_reconcile_ns (doc , newchild );
0 commit comments