Skip to content

Commit 4cc6b03

Browse files
committed
Fix GH-11404: DOMDocument::savexml and friends ommit xmlns="" declaration for null namespace, creating incorrect xml representation of the DOM
The NULL namespace is only correct when there is no default namespace override. When there is, we need to manually set it to the empty string namespace.
1 parent f2d673f commit 4cc6b03

File tree

5 files changed

+172
-9
lines changed

5 files changed

+172
-9
lines changed

ext/dom/document.c

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -878,6 +878,10 @@ PHP_METHOD(DOMDocument, createElementNS)
878878

879879
if (errorcode == 0) {
880880
if (xmlValidateName((xmlChar *) localname, 0) == 0) {
881+
/* https://dom.spec.whatwg.org/#validate-and-extract: requires the empty string uri to be set to NULL */
882+
if (uri_len == 0) {
883+
uri = NULL;
884+
}
881885
nodep = xmlNewDocNode(docp, NULL, (xmlChar *) localname, (xmlChar *) value);
882886
if (nodep != NULL && uri != NULL) {
883887
nsptr = xmlSearchNsByHref(nodep->doc, nodep, (xmlChar *) uri);

ext/dom/element.c

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ PHP_METHOD(DOMElement, __construct)
5656
if (uri_len > 0) {
5757
errorcode = dom_check_qname(name, &localname, &prefix, uri_len, name_len);
5858
if (errorcode == 0) {
59+
/* https://dom.spec.whatwg.org/#validate-and-extract: requires the empty string uri to be set to NULL */
60+
if (uri_len == 0) {
61+
uri = NULL;
62+
}
5963
nodep = xmlNewNode (NULL, (xmlChar *)localname);
6064
if (nodep != NULL && uri != NULL) {
6165
nsptr = dom_get_ns(nodep, uri, &errorcode, prefix);

ext/dom/node.c

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -531,7 +531,6 @@ Since: DOM Level 2
531531
int dom_node_namespace_uri_read(dom_object *obj, zval *retval)
532532
{
533533
xmlNode *nodep = dom_object_get_node(obj);
534-
char *str = NULL;
535534

536535
if (nodep == NULL) {
537536
php_dom_throw_error(INVALID_STATE_ERR, 1);
@@ -543,20 +542,19 @@ int dom_node_namespace_uri_read(dom_object *obj, zval *retval)
543542
case XML_ATTRIBUTE_NODE:
544543
case XML_NAMESPACE_DECL:
545544
if (nodep->ns != NULL) {
546-
str = (char *) nodep->ns->href;
545+
char *str = (char *) nodep->ns->href;
546+
/* https://dom.spec.whatwg.org/#concept-attribute: namespaceUri is "null or a non-empty string" */
547+
if (str != NULL && str[0] != '\0') {
548+
ZVAL_STRING(retval, str);
549+
return SUCCESS;
550+
}
547551
}
548552
break;
549553
default:
550-
str = NULL;
551554
break;
552555
}
553556

554-
if (str != NULL) {
555-
ZVAL_STRING(retval, str);
556-
} else {
557-
ZVAL_NULL(retval);
558-
}
559-
557+
ZVAL_NULL(retval);
560558
return SUCCESS;
561559
}
562560

ext/dom/php_dom.c

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1442,10 +1442,25 @@ static void dom_reconcile_ns_internal(xmlDocPtr doc, xmlNodePtr nodep)
14421442
}
14431443
}
14441444

1445+
static void dom_check_default_namespace(xmlDocPtr doc, xmlNodePtr nodep)
1446+
{
1447+
if (nodep->ns == NULL) {
1448+
xmlNsPtr default_ns = xmlSearchNs(doc, nodep->parent, NULL);
1449+
if (default_ns != NULL && default_ns->href != NULL && default_ns->href[0] != '\0') {
1450+
/* The node uses the default empty namespace, but the current default namespace is non-empty.
1451+
* We can't unconditionally do this because otherwise libxml2 creates an xmlns="" declaration.
1452+
* Note: there's no point searching the oldNs list, because we haven't found it in the tree anyway.
1453+
* Ideally this would be pre-allocated but unfortunately libxml2 doesn't offer such a functionality. */
1454+
xmlSetNs(nodep, xmlNewNs(nodep, (const xmlChar *) "", NULL));
1455+
}
1456+
}
1457+
}
1458+
14451459
void dom_reconcile_ns(xmlDocPtr doc, xmlNodePtr nodep) /* {{{ */
14461460
{
14471461
if (nodep->type == XML_ELEMENT_NODE) {
14481462
dom_reconcile_ns_internal(doc, nodep);
1463+
dom_check_default_namespace(doc, nodep);
14491464
xmlReconciliateNs(doc, nodep);
14501465
}
14511466
}
@@ -1471,6 +1486,7 @@ static void dom_reconcile_ns_list_internal(xmlDocPtr doc, xmlNodePtr nodep, xmlN
14711486
void dom_reconcile_ns_list(xmlDocPtr doc, xmlNodePtr nodep, xmlNodePtr last)
14721487
{
14731488
dom_reconcile_ns_list_internal(doc, nodep, last);
1489+
dom_check_default_namespace(doc, nodep);
14741490
/* Outside of the recursion above because xmlReconciliateNs() performs its own recursion. */
14751491
while (true) {
14761492
xmlReconciliateNs(doc, nodep);

ext/dom/tests/gh11404.phpt

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
--TEST--
2+
GH-11404: DOMDocument::savexml and friends ommit xmlns="" declaration for null namespace, creating incorrect xml representation of the DOM
3+
--EXTENSIONS--
4+
dom
5+
--FILE--
6+
<?php
7+
8+
echo "-- Test append and attributes: with default namespace variation --\n";
9+
10+
function testAppendAndAttributes($dom) {
11+
$nodeA = $dom->createElement('a');
12+
$nodeB = $dom->createElementNS(null, 'b');
13+
$nodeC = $dom->createElementNS('', 'c');
14+
$nodeD = $dom->createElement('d');
15+
$nodeD->setAttributeNS('some:ns', 'x:attrib', 'val');
16+
$nodeE = $dom->createElementNS('some:ns', 'e');
17+
// And these two respect the default ns.
18+
$nodeE->setAttributeNS(null, 'attrib1', 'val');
19+
$nodeE->setAttributeNS('', 'attrib2', 'val');
20+
21+
$dom->documentElement->appendChild($nodeA);
22+
$dom->documentElement->appendChild($nodeB);
23+
$dom->documentElement->appendChild($nodeC);
24+
$dom->documentElement->appendChild($nodeD);
25+
$dom->documentElement->appendChild($nodeE);
26+
27+
var_dump($nodeA->namespaceURI);
28+
var_dump($nodeB->namespaceURI);
29+
var_dump($nodeC->namespaceURI);
30+
var_dump($nodeD->namespaceURI);
31+
var_dump($nodeE->namespaceURI);
32+
33+
$subtree = $dom->createElement('subtree');
34+
$subtree->appendChild($dom->createElementNS('some:ns', 'subtreechild1'));
35+
$subtree->firstElementChild->appendChild($dom->createElement('subtreechild2'));
36+
$dom->documentElement->appendChild($subtree);
37+
38+
echo $dom->saveXML();
39+
}
40+
41+
$dom1 = new DOMDocument;
42+
$dom1->loadXML('<?xml version="1.0" ?><with xmlns="some:ns" />');
43+
testAppendAndAttributes($dom1);
44+
45+
echo "-- Test append and attributes: without default namespace variation --\n";
46+
47+
$dom1 = new DOMDocument;
48+
$dom1->loadXML('<?xml version="1.0" ?><with/>');
49+
testAppendAndAttributes($dom1);
50+
51+
echo "-- Test import --\n";
52+
53+
function testImport(?string $href, string $toBeImported) {
54+
$dom1 = new DOMDocument;
55+
$decl = $href === NULL ? '' : "xmlns=\"$href\"";
56+
$dom1->loadXML('<?xml version="1.0" ?><with ' . $decl . '/>');
57+
58+
$dom2 = new DOMDocument;
59+
$dom2->loadXML('<?xml version="1.0" ?>' . $toBeImported);
60+
61+
$dom1->documentElement->append(
62+
$imported = $dom1->importNode($dom2->documentElement, true)
63+
);
64+
65+
var_dump($imported->namespaceURI);
66+
67+
echo $dom1->saveXML();
68+
}
69+
70+
testImport(null, '<none/>');
71+
testImport('', '<none/>');
72+
testImport('some:ns', '<none/>');
73+
testImport('', '<none><div xmlns="some:ns"/></none>');
74+
testImport('some:ns', '<none xmlns="some:ns"><div xmlns=""/></none>');
75+
76+
echo "-- Namespace URI comparison --\n";
77+
78+
$dom1 = new DOMDocument;
79+
$dom1->loadXML('<?xml version="1.0"?><test xmlns="a:b"><div/></test>');
80+
var_dump($dom1->firstElementChild->namespaceURI);
81+
var_dump($dom1->firstElementChild->firstElementChild->namespaceURI);
82+
83+
$dom1 = new DOMDocument;
84+
$dom1->appendChild($dom1->createElementNS('a:b', 'parent'));
85+
$dom1->firstElementChild->appendChild($dom1->createElementNS('a:b', 'child1'));
86+
$dom1->firstElementChild->appendChild($second = $dom1->createElement('child2'));
87+
var_dump($dom1->firstElementChild->namespaceURI);
88+
var_dump($dom1->firstElementChild->firstElementChild->namespaceURI);
89+
var_dump($second->namespaceURI);
90+
var_dump($dom1->saveXML());
91+
92+
$dom1 = new DOMDocument;
93+
$dom1->loadXML('<?xml version="1.0"?><test xmlns="a:b"/>');
94+
var_dump($dom1->firstElementChild->namespaceURI);
95+
$dom1->firstElementChild->appendChild($dom1->createElementNS('a:b', 'tag'));
96+
var_dump($dom1->firstElementChild->firstElementChild->namespaceURI);
97+
?>
98+
--EXPECT--
99+
-- Test append and attributes: with default namespace variation --
100+
NULL
101+
NULL
102+
NULL
103+
NULL
104+
string(7) "some:ns"
105+
<?xml version="1.0"?>
106+
<with xmlns="some:ns"><a xmlns=""/><b xmlns=""/><c xmlns=""/><d xmlns:x="some:ns" xmlns="" x:attrib="val"/><e attrib1="val" attrib2="val"/><subtree xmlns="" xmlns:default="some:ns"><default:subtreechild1 xmlns="some:ns"><subtreechild2 xmlns=""/></default:subtreechild1></subtree></with>
107+
-- Test append and attributes: without default namespace variation --
108+
NULL
109+
NULL
110+
NULL
111+
NULL
112+
string(7) "some:ns"
113+
<?xml version="1.0"?>
114+
<with><a/><b/><c/><d xmlns:x="some:ns" x:attrib="val"/><e xmlns="some:ns" attrib1="val" attrib2="val"/><subtree xmlns:default="some:ns" xmlns:default1=""><default:subtreechild1 xmlns="some:ns"><default1:subtreechild2 xmlns=""/></default:subtreechild1></subtree></with>
115+
-- Test import --
116+
NULL
117+
<?xml version="1.0"?>
118+
<with><none/></with>
119+
NULL
120+
<?xml version="1.0"?>
121+
<with xmlns=""><none/></with>
122+
NULL
123+
<?xml version="1.0"?>
124+
<with xmlns="some:ns"><none xmlns=""/></with>
125+
NULL
126+
<?xml version="1.0"?>
127+
<with xmlns=""><none xmlns:default="some:ns"><default:div xmlns="some:ns"/></none></with>
128+
string(7) "some:ns"
129+
<?xml version="1.0"?>
130+
<with xmlns="some:ns"><none><div xmlns=""/></none></with>
131+
-- Namespace URI comparison --
132+
string(3) "a:b"
133+
string(3) "a:b"
134+
string(3) "a:b"
135+
string(3) "a:b"
136+
NULL
137+
string(79) "<?xml version="1.0"?>
138+
<parent xmlns="a:b"><child1/><child2 xmlns=""/></parent>
139+
"
140+
string(3) "a:b"
141+
string(3) "a:b"

0 commit comments

Comments
 (0)