Skip to content

Commit 4770876

Browse files
committed
Merge branch 'PHP-8.2'
* PHP-8.2: Fix phpGH-11404: DOMDocument::savexml and friends ommit xmlns="" declaration for null namespace, creating incorrect xml representation of the DOM
2 parents 1518443 + bb3e5a8 commit 4770876

File tree

6 files changed

+214
-10
lines changed

6 files changed

+214
-10
lines changed

ext/dom/document.c

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

878878
if (errorcode == 0) {
879879
if (xmlValidateName((xmlChar *) localname, 0) == 0) {
880+
/* https://dom.spec.whatwg.org/#validate-and-extract: demands us to set an empty string uri to NULL */
881+
if (uri_len == 0) {
882+
uri = NULL;
883+
}
880884
nodep = xmlNewDocNode(docp, NULL, (xmlChar *) localname, (xmlChar *) value);
881885
if (nodep != NULL && uri != NULL) {
882886
xmlNsPtr 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: demands us to set an empty string uri 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
@@ -532,7 +532,6 @@ Since: DOM Level 2
532532
int dom_node_namespace_uri_read(dom_object *obj, zval *retval)
533533
{
534534
xmlNode *nodep = dom_object_get_node(obj);
535-
char *str = NULL;
536535

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

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

ext/dom/php_dom.c

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1491,13 +1491,33 @@ static void dom_libxml_reconcile_ensure_namespaces_are_declared(xmlNodePtr nodep
14911491
xmlDOMWrapReconcileNamespaces(&dummy_ctxt, nodep, /* options */ 0);
14921492
}
14931493

1494+
static bool dom_must_replace_namespace_by_empty_default(xmlDocPtr doc, xmlNodePtr nodep)
1495+
{
1496+
xmlNsPtr default_ns = xmlSearchNs(doc, nodep->parent, NULL);
1497+
return default_ns != NULL && default_ns->href != NULL && default_ns->href[0] != '\0';
1498+
}
1499+
1500+
static void dom_replace_namespace_by_empty_default(xmlDocPtr doc, xmlNodePtr nodep)
1501+
{
1502+
ZEND_ASSERT(nodep->ns == NULL);
1503+
/* The node uses the default empty namespace, but the current default namespace is non-empty.
1504+
* We can't unconditionally do this because otherwise libxml2 creates an xmlns="" declaration.
1505+
* Note: there's no point searching the oldNs list, because we haven't found it in the tree anyway.
1506+
* Ideally this would be pre-allocated but unfortunately libxml2 doesn't offer such a functionality. */
1507+
xmlSetNs(nodep, xmlNewNs(nodep, (const xmlChar *) "", NULL));
1508+
}
1509+
14941510
void dom_reconcile_ns(xmlDocPtr doc, xmlNodePtr nodep) /* {{{ */
14951511
{
14961512
/* Although the node type will be checked by the libxml2 API,
14971513
* we still want to do the internal reconciliation conditionally. */
14981514
if (nodep->type == XML_ELEMENT_NODE) {
14991515
dom_reconcile_ns_internal(doc, nodep, nodep->parent);
15001516
dom_libxml_reconcile_ensure_namespaces_are_declared(nodep);
1517+
/* Check nodep->ns first to avoid an expensive lookup. */
1518+
if (nodep->ns == NULL && dom_must_replace_namespace_by_empty_default(doc, nodep)) {
1519+
dom_replace_namespace_by_empty_default(doc, nodep);
1520+
}
15011521
}
15021522
}
15031523
/* }}} */
@@ -1521,12 +1541,30 @@ static void dom_reconcile_ns_list_internal(xmlDocPtr doc, xmlNodePtr nodep, xmlN
15211541

15221542
void dom_reconcile_ns_list(xmlDocPtr doc, xmlNodePtr nodep, xmlNodePtr last)
15231543
{
1544+
bool did_compute_must_replace_namespace_by_empty_default = false;
1545+
bool must_replace_namespace_by_empty_default = false;
1546+
15241547
dom_reconcile_ns_list_internal(doc, nodep, last, nodep->parent);
1548+
15251549
/* The loop is outside of the recursion in the above call because
15261550
* dom_libxml_reconcile_ensure_namespaces_are_declared() performs its own recursion. */
15271551
while (true) {
15281552
/* The internal libxml2 call will already check the node type, no need for us to do it here. */
15291553
dom_libxml_reconcile_ensure_namespaces_are_declared(nodep);
1554+
1555+
/* We don't have to handle the children, because if their ns's are NULL they'll just take on the default
1556+
* which should've been reconciled before. */
1557+
if (nodep->ns == NULL) {
1558+
/* This is an optimistic approach: we assume that most of the time we don't need the result of the computation. */
1559+
if (!did_compute_must_replace_namespace_by_empty_default) {
1560+
did_compute_must_replace_namespace_by_empty_default = true;
1561+
must_replace_namespace_by_empty_default = dom_must_replace_namespace_by_empty_default(doc, nodep);
1562+
}
1563+
if (must_replace_namespace_by_empty_default) {
1564+
dom_replace_namespace_by_empty_default(doc, nodep);
1565+
}
1566+
}
1567+
15301568
if (nodep == last) {
15311569
break;
15321570
}

ext/dom/tests/bug47530.phpt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ test_appendChild_with_shadowing();
121121
<html xmlns="https://php.net/something" xmlns:ns="https://php.net/whatever"><element ns:foo="https://php.net/bar"/></html>
122122
-- Test document fragment without import --
123123
<?xml version="1.0"?>
124-
<html xmlns=""><element xmlns:foo="https://php.net/bar"><foo:bar/><bar xmlns=""/></element></html>
124+
<html xmlns=""><element xmlns:foo="https://php.net/bar"><foo:bar/><bar/></element></html>
125125
string(7) "foo:bar"
126126
string(19) "https://php.net/bar"
127127
-- Test document import --

ext/dom/tests/gh11404.phpt

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
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+
echo $dom->saveXML();
34+
35+
// Create a subtree without using a fragment
36+
$subtree = $dom->createElement('subtree');
37+
$subtree->appendChild($dom->createElementNS('some:ns', 'subtreechild1'));
38+
$subtree->firstElementChild->appendChild($dom->createElement('subtreechild2'));
39+
$dom->documentElement->appendChild($subtree);
40+
41+
echo $dom->saveXML();
42+
43+
// Create a subtree with the use of a fragment
44+
$subtree = $dom->createDocumentFragment();
45+
$subtree->appendChild($child3 = $dom->createElement('child3'));
46+
$child3->appendChild($dom->createElement('child4'));
47+
$subtree->appendChild($dom->createElement('child5'));
48+
$dom->documentElement->appendChild($subtree);
49+
50+
echo $dom->saveXML();
51+
}
52+
53+
$dom1 = new DOMDocument;
54+
$dom1->loadXML('<?xml version="1.0" ?><with xmlns="some:ns" />');
55+
testAppendAndAttributes($dom1);
56+
57+
echo "-- Test append and attributes: without default namespace variation --\n";
58+
59+
$dom1 = new DOMDocument;
60+
$dom1->loadXML('<?xml version="1.0" ?><with/>');
61+
testAppendAndAttributes($dom1);
62+
63+
echo "-- Test import --\n";
64+
65+
function testImport(?string $href, string $toBeImported) {
66+
$dom1 = new DOMDocument;
67+
$decl = $href === NULL ? '' : "xmlns=\"$href\"";
68+
$dom1->loadXML('<?xml version="1.0" ?><with ' . $decl . '/>');
69+
70+
$dom2 = new DOMDocument;
71+
$dom2->loadXML('<?xml version="1.0" ?>' . $toBeImported);
72+
73+
$dom1->documentElement->append(
74+
$imported = $dom1->importNode($dom2->documentElement, true)
75+
);
76+
77+
var_dump($imported->namespaceURI);
78+
79+
echo $dom1->saveXML();
80+
}
81+
82+
testImport(null, '<none/>');
83+
testImport('', '<none/>');
84+
testImport('some:ns', '<none/>');
85+
testImport('', '<none><div xmlns="some:ns"/></none>');
86+
testImport('some:ns', '<none xmlns="some:ns"><div xmlns=""/></none>');
87+
88+
echo "-- Namespace URI comparison --\n";
89+
90+
$dom1 = new DOMDocument;
91+
$dom1->loadXML('<?xml version="1.0"?><test xmlns="a:b"><div/></test>');
92+
var_dump($dom1->firstElementChild->namespaceURI);
93+
var_dump($dom1->firstElementChild->firstElementChild->namespaceURI);
94+
95+
$dom1 = new DOMDocument;
96+
$dom1->appendChild($dom1->createElementNS('a:b', 'parent'));
97+
$dom1->firstElementChild->appendChild($dom1->createElementNS('a:b', 'child1'));
98+
$dom1->firstElementChild->appendChild($second = $dom1->createElement('child2'));
99+
var_dump($dom1->firstElementChild->namespaceURI);
100+
var_dump($dom1->firstElementChild->firstElementChild->namespaceURI);
101+
var_dump($second->namespaceURI);
102+
echo $dom1->saveXML();
103+
104+
$dom1 = new DOMDocument;
105+
$dom1->loadXML('<?xml version="1.0"?><test xmlns="a:b"/>');
106+
var_dump($dom1->firstElementChild->namespaceURI);
107+
$dom1->firstElementChild->appendChild($dom1->createElementNS('a:b', 'tag'));
108+
var_dump($dom1->firstElementChild->firstElementChild->namespaceURI);
109+
?>
110+
--EXPECT--
111+
-- Test append and attributes: with default namespace variation --
112+
NULL
113+
NULL
114+
NULL
115+
NULL
116+
string(7) "some:ns"
117+
<?xml version="1.0"?>
118+
<with xmlns="some:ns"><a xmlns=""/><b xmlns=""/><c xmlns=""/><d xmlns:x="some:ns" xmlns="" x:attrib="val"/><e attrib1="val" attrib2="val"/></with>
119+
<?xml version="1.0"?>
120+
<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=""><subtreechild1 xmlns="some:ns"><subtreechild2 xmlns=""/></subtreechild1></subtree></with>
121+
<?xml version="1.0"?>
122+
<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=""><subtreechild1 xmlns="some:ns"><subtreechild2 xmlns=""/></subtreechild1></subtree><child3 xmlns=""><child4/></child3><child5 xmlns=""/></with>
123+
-- Test append and attributes: without default namespace variation --
124+
NULL
125+
NULL
126+
NULL
127+
NULL
128+
string(7) "some:ns"
129+
<?xml version="1.0"?>
130+
<with><a/><b/><c/><d xmlns:x="some:ns" x:attrib="val"/><e xmlns="some:ns" attrib1="val" attrib2="val"/></with>
131+
<?xml version="1.0"?>
132+
<with><a/><b/><c/><d xmlns:x="some:ns" x:attrib="val"/><e xmlns="some:ns" attrib1="val" attrib2="val"/><subtree><subtreechild1 xmlns="some:ns"><subtreechild2 xmlns=""/></subtreechild1></subtree></with>
133+
<?xml version="1.0"?>
134+
<with><a/><b/><c/><d xmlns:x="some:ns" x:attrib="val"/><e xmlns="some:ns" attrib1="val" attrib2="val"/><subtree><subtreechild1 xmlns="some:ns"><subtreechild2 xmlns=""/></subtreechild1></subtree><child3><child4/></child3><child5/></with>
135+
-- Test import --
136+
NULL
137+
<?xml version="1.0"?>
138+
<with><none/></with>
139+
NULL
140+
<?xml version="1.0"?>
141+
<with xmlns=""><none/></with>
142+
NULL
143+
<?xml version="1.0"?>
144+
<with xmlns="some:ns"><none xmlns=""/></with>
145+
NULL
146+
<?xml version="1.0"?>
147+
<with xmlns=""><none><div xmlns="some:ns"/></none></with>
148+
string(7) "some:ns"
149+
<?xml version="1.0"?>
150+
<with xmlns="some:ns"><none><div xmlns=""/></none></with>
151+
-- Namespace URI comparison --
152+
string(3) "a:b"
153+
string(3) "a:b"
154+
string(3) "a:b"
155+
string(3) "a:b"
156+
NULL
157+
<?xml version="1.0"?>
158+
<parent xmlns="a:b"><child1/><child2 xmlns=""/></parent>
159+
string(3) "a:b"
160+
string(3) "a:b"

0 commit comments

Comments
 (0)