[php-src] master: Merge branch 'PHP-8.5'

Ilia Alshanetsky <[email protected]> Mon, 20 Jul 2026 17:16:20 +0000
Newsgroups gmane.comp.php.cvs.general
Message-ID <[email protected]>
Author: Ilia Alshanetsky (iliaal)
Date: 2026-07-20T13:09:49-04:00

Commit: https://github.com/php/php-src/commit/1c7f50ae42f1cb7388d2c6a7c00f2b7636373dae
Raw diff: https://github.com/php/php-src/commit/1c7f50ae42f1cb7388d2c6a7c00f2b7636373dae.diff

Merge branch 'PHP-8.5'

* PHP-8.5:
  Fix GH-22825: DOM attribute methods on a DTD default attribute

Changed paths:
  A  ext/dom/tests/gh22825.phpt
  M  NEWS
  M  ext/dom/element.c


Diff:

diff --git a/NEWS b/NEWS
index 7dd8530af905..7a00f15c2ba4 100644
--- a/NEWS
+++ b/NEWS
@@ -5,6 +5,10 @@ PHP                                                                        NEWS
 - Core:
   . Implemented partial function application RFC. (Arnaud)
 
+- DOM:
+  . Fixed bug GH-22825 (DOMElement::setAttribute() fails silently when the DTD
+    declares a default value for the attribute). (iliaal)
+
 - GMP:
   . Fixed GMP power and shift operators to reject GMP right operands outside
     the unsigned long range instead of silently truncating them. (Weilin Du)
diff --git a/ext/dom/element.c b/ext/dom/element.c
index 8e6102e9658e..71fa39b59b5a 100644
--- a/ext/dom/element.c
+++ b/ext/dom/element.c
@@ -464,6 +464,8 @@ PHP_METHOD(DOMElement, setAttribute)
 					break;
 				case XML_NAMESPACE_DECL:
 					RETURN_FALSE;
+				case XML_ATTRIBUTE_DECL:
+					break;
 				default: ZEND_UNREACHABLE();
 			}
 		}
@@ -591,6 +593,8 @@ static bool dom_remove_attribute(xmlNodePtr thisp, xmlNodePtr attrp)
 
 			break;
 		}
+		case XML_ATTRIBUTE_DECL:
+			return false;
 		default: ZEND_UNREACHABLE();
 	}
 	return true;
@@ -720,7 +724,11 @@ static void dom_element_set_attribute_node_common(INTERNAL_FUNCTION_PARAMETERS,
 		existattrp = xmlHasProp(nodep, attrp->name);
 	}
 
-	if (existattrp != NULL && existattrp->type != XML_ATTRIBUTE_DECL) {
+	if (existattrp != NULL && existattrp->type == XML_ATTRIBUTE_DECL) {
+		existattrp = NULL;
+	}
+
+	if (existattrp != NULL) {
 		if ((oldobj = php_dom_object_get_data((xmlNodePtr) existattrp)) != NULL &&
 			((php_libxml_node_ptr *)oldobj->ptr)->node == (xmlNodePtr) attrp)
 		{
@@ -1933,8 +1941,7 @@ PHP_METHOD(DOMElement, toggleAttribute)
 
 	/* Step 5 */
 	if (force_is_null || !force) {
-		dom_remove_attribute(thisp, attribute);
-		retval = false;
+		retval = !dom_remove_attribute(thisp, attribute);
 		goto out;
 	}
 
diff --git a/ext/dom/tests/gh22825.phpt b/ext/dom/tests/gh22825.phpt
new file mode 100644
index 000000000000..987ed57a1454
--- /dev/null
+++ b/ext/dom/tests/gh22825.phpt
@@ -0,0 +1,98 @@
+--TEST--
+GH-22825 (DOMElement::setAttribute() reaches EMPTY_SWITCH_DEFAULT_CASE() with DTD #FIXED default attributes)
+--EXTENSIONS--
+dom
+--FILE--
+<?php
+$cases = [
+    ['<!ATTLIST root A CDATA #FIXED "d">', '<root/>', 'A'],
+    ['<!ATTLIST root A CDATA "d">', '<root/>', 'A'],
+    ['<!ATTLIST root p:A CDATA #FIXED "d">', '<root xmlns:p="urn:p"/>', 'p:A'],
+];
+
+function element(string $attlist, string $root): DOMElement {
+    $doc = new DOMDocument();
+    $doc->loadXML("<!DOCTYPE root [$attlist]>$root");
+    return $doc->documentElement;
+}
+
+foreach ($cases as [$attlist, $root, $name]) {
+    echo "--- $attlist ---\n";
+
+    $el = element($attlist, $root);
+    echo "hasAttribute: ";
+    var_dump($el->hasAttribute($name));
+    echo "getAttribute: ";
+    var_dump($el->getAttribute($name));
+
+    $el = element($attlist, $root);
+    $result = $el->setAttribute($name, 'v');
+    echo "setAttribute: ", is_object($result) ? $result::class : var_export($result, true), "\n";
+    echo "after setAttribute: ", trim($el->ownerDocument->saveXML($el)), "\n";
+
+    $el = element($attlist, $root);
+    echo "removeAttribute: ";
+    var_dump($el->removeAttribute($name));
+    echo "still present after removeAttribute: ";
+    var_dump($el->hasAttribute($name));
+
+    $el = element($attlist, $root);
+    echo "toggleAttribute(false): ";
+    var_dump($el->toggleAttribute($name, false));
+    echo "still present after toggleAttribute(false): ";
+    var_dump($el->hasAttribute($name));
+
+    $el = element($attlist, $root);
+    echo "toggleAttribute(true): ";
+    var_dump($el->toggleAttribute($name, true));
+    echo "still present after toggleAttribute(true): ";
+    var_dump($el->hasAttribute($name));
+
+    $el = element($attlist, $root);
+    $attr = $el->ownerDocument->createAttribute($name);
+    $attr->value = 'z';
+    echo "setAttributeNode: ";
+    var_dump($el->setAttributeNode($attr));
+    echo "after setAttributeNode: ", trim($el->ownerDocument->saveXML($el)), "\n";
+}
+?>
+--EXPECT--
+--- <!ATTLIST root A CDATA #FIXED "d"> ---
+hasAttribute: bool(true)
+getAttribute: string(1) "d"
+setAttribute: DOMAttr
+after setAttribute: <root A="v"/>
+removeAttribute: bool(false)
+still present after removeAttribute: bool(true)
+toggleAttribute(false): bool(true)
+still present after toggleAttribute(false): bool(true)
+toggleAttribute(true): bool(true)
+still present after toggleAttribute(true): bool(true)
+setAttributeNode: NULL
+after setAttributeNode: <root A="z"/>
+--- <!ATTLIST root A CDATA "d"> ---
+hasAttribute: bool(true)
+getAttribute: string(1) "d"
+setAttribute: DOMAttr
+after setAttribute: <root A="v"/>
+removeAttribute: bool(false)
+still present after removeAttribute: bool(true)
+toggleAttribute(false): bool(true)
+still present after toggleAttribute(false): bool(true)
+toggleAttribute(true): bool(true)
+still present after toggleAttribute(true): bool(true)
+setAttributeNode: NULL
+after setAttributeNode: <root A="z"/>
+--- <!ATTLIST root p:A CDATA #FIXED "d"> ---
+hasAttribute: bool(true)
+getAttribute: string(1) "d"
+setAttribute: DOMAttr
+after setAttribute: <root xmlns:p="urn:p" p:A="v"/>
+removeAttribute: bool(false)
+still present after removeAttribute: bool(true)
+toggleAttribute(false): bool(true)
+still present after toggleAttribute(false): bool(true)
+toggleAttribute(true): bool(true)
+still present after toggleAttribute(true): bool(true)
+setAttributeNode: NULL
+after setAttributeNode: <root xmlns:p="urn:p" p:A="z"/>