@@ -418,9 +418,68 @@ PHP_METHOD(DOMElement, setAttribute)
418418}
419419/* }}} end dom_element_set_attribute */
420420
421- static bool dom_remove_attribute (xmlNodePtr attrp )
421+ typedef struct {
422+ xmlNodePtr current_node ;
423+ xmlNsPtr defined_ns ;
424+ } dom_deep_ns_redef_item ;
425+
426+ /* Reconciliation for a *single* namespace, but reconciles *closest* to the subtree needing it. */
427+ static void dom_deep_ns_redef (xmlNodePtr node , xmlNsPtr ns_to_redefine )
422428{
429+ size_t worklist_capacity = 128 ;
430+ dom_deep_ns_redef_item * worklist = emalloc (sizeof (dom_deep_ns_redef_item ) * worklist_capacity );
431+ worklist [0 ].current_node = node ;
432+ worklist [0 ].defined_ns = NULL ;
433+ size_t worklist_size = 1 ;
434+
435+ while (worklist_size > 0 ) {
436+ worklist_size -- ;
437+ dom_deep_ns_redef_item * current_worklist_item = & worklist [worklist_size ];
438+ ZEND_ASSERT (current_worklist_item -> current_node -> type == XML_ELEMENT_NODE );
439+ xmlNsPtr defined_ns = current_worklist_item -> defined_ns ;
440+
441+ if (current_worklist_item -> current_node -> ns == ns_to_redefine ) {
442+ if (defined_ns == NULL ) {
443+ defined_ns = xmlNewNs (current_worklist_item -> current_node , ns_to_redefine -> href , ns_to_redefine -> prefix );
444+ }
445+ current_worklist_item -> current_node -> ns = defined_ns ;
446+ }
447+
448+ for (xmlAttrPtr attr = current_worklist_item -> current_node -> properties ; attr ; attr = attr -> next ) {
449+ if (attr -> ns == ns_to_redefine ) {
450+ if (defined_ns == NULL ) {
451+ defined_ns = xmlNewNs (current_worklist_item -> current_node , ns_to_redefine -> href , ns_to_redefine -> prefix );
452+ }
453+ attr -> ns = defined_ns ;
454+ }
455+ }
456+
457+ for (xmlNodePtr child = current_worklist_item -> current_node -> children ; child ; child = child -> next ) {
458+ if (child -> type != XML_ELEMENT_NODE ) {
459+ continue ;
460+ }
461+ if (worklist_size == worklist_capacity ) {
462+ if (UNEXPECTED (worklist_capacity >= SIZE_MAX / 3 * 2 / sizeof (dom_deep_ns_redef_item ))) {
463+ /* Shouldn't be possible to hit, but checked for safety anyway */
464+ return ;
465+ }
466+ worklist_capacity = worklist_capacity * 3 / 2 ;
467+ worklist = erealloc (worklist , sizeof (dom_deep_ns_redef_item ) * worklist_capacity );
468+ }
469+ worklist [worklist_size ].current_node = child ;
470+ worklist [worklist_size ].defined_ns = defined_ns ;
471+ worklist_size ++ ;
472+ }
473+ }
474+
475+ efree (worklist );
476+ }
477+
478+ static bool dom_remove_attribute (xmlNodePtr thisp , xmlNodePtr attrp )
479+ {
480+ ZEND_ASSERT (thisp != NULL );
423481 ZEND_ASSERT (attrp != NULL );
482+
424483 switch (attrp -> type ) {
425484 case XML_ATTRIBUTE_NODE :
426485 if (php_dom_object_get_data (attrp ) == NULL ) {
@@ -431,8 +490,42 @@ static bool dom_remove_attribute(xmlNodePtr attrp)
431490 xmlUnlinkNode (attrp );
432491 }
433492 break ;
434- case XML_NAMESPACE_DECL :
435- return false;
493+ case XML_NAMESPACE_DECL : {
494+ /* They will always be removed, but can be re-added.
495+ *
496+ * If any reference was left to the namespace, the only effect is that
497+ * the definition is potentially moved closer to the element using it.
498+ * If no reference was left, it is actually removed. */
499+ xmlNsPtr ns = (xmlNsPtr ) attrp ;
500+ if (thisp -> nsDef == ns ) {
501+ thisp -> nsDef = ns -> next ;
502+ } else if (thisp -> nsDef != NULL ) {
503+ xmlNsPtr prev = thisp -> nsDef ;
504+ xmlNsPtr cur = prev -> next ;
505+ while (cur ) {
506+ if (cur == ns ) {
507+ prev -> next = cur -> next ;
508+ break ;
509+ }
510+ prev = cur ;
511+ cur = cur -> next ;
512+ }
513+ } else {
514+ /* defensive: attrp not defined in thisp ??? */
515+ #if ZEND_DEBUG
516+ ZEND_UNREACHABLE ();
517+ #endif
518+ break ; /* defensive */
519+ }
520+
521+ ns -> next = NULL ;
522+ php_libxml_set_old_ns (thisp -> doc , ns ); /* note: can't deallocate as it might be referenced by a "fake namespace node" */
523+ /* xmlReconciliateNs() redefines at the top of the tree instead of closest to the child, own reconciliation here.
524+ * Similarly, the DOM version has other issues too (see dom_libxml_reconcile_ensure_namespaces_are_declared). */
525+ dom_deep_ns_redef (thisp , ns );
526+
527+ break ;
528+ }
436529 EMPTY_SWITCH_DEFAULT_CASE ();
437530 }
438531 return true;
@@ -461,7 +554,7 @@ PHP_METHOD(DOMElement, removeAttribute)
461554 RETURN_FALSE ;
462555 }
463556
464- RETURN_BOOL (dom_remove_attribute (attrp ));
557+ RETURN_BOOL (dom_remove_attribute (nodep , attrp ));
465558}
466559/* }}} end dom_element_remove_attribute */
467560
@@ -1426,37 +1519,7 @@ PHP_METHOD(DOMElement, toggleAttribute)
14261519
14271520 /* Step 5 */
14281521 if (force_is_null || !force ) {
1429- if (attribute -> type == XML_NAMESPACE_DECL ) {
1430- /* The behaviour isn't defined by spec, but by observing browsers I found
1431- * that you can remove the nodes, but they'll get reconciled.
1432- * So if any reference was left to the namespace, the only effect is that
1433- * the definition is potentially moved closer to the element using it.
1434- * If no reference was left, it is actually removed. */
1435- xmlNsPtr ns = (xmlNsPtr ) attribute ;
1436- if (thisp -> nsDef == ns ) {
1437- thisp -> nsDef = ns -> next ;
1438- } else if (thisp -> nsDef != NULL ) {
1439- xmlNsPtr prev = thisp -> nsDef ;
1440- xmlNsPtr cur = prev -> next ;
1441- while (cur ) {
1442- if (cur == ns ) {
1443- prev -> next = cur -> next ;
1444- break ;
1445- }
1446- prev = cur ;
1447- cur = cur -> next ;
1448- }
1449- }
1450-
1451- ns -> next = NULL ;
1452- php_libxml_set_old_ns (thisp -> doc , ns );
1453- dom_reconcile_ns (thisp -> doc , thisp );
1454- } else {
1455- /* TODO: in the future when namespace bugs are fixed,
1456- * the above if-branch should be merged into this called function
1457- * such that the removal will work properly with all APIs. */
1458- dom_remove_attribute (attribute );
1459- }
1522+ dom_remove_attribute (thisp , attribute );
14601523 retval = false;
14611524 goto out ;
14621525 }
0 commit comments