Revert "Fix GH-11404: DOMDocument::savexml and friends ommit xmlns="" declaration for null namespace, creating incorrect xml representation of the DOM"

This reverts commit 7eb3e9cd17.

Although the fix follows the spec, it causes issues because a lot of old
code assumes the incorrect behaviour PHP had since a long time.
We cannot do this yet, especially not in a stable release.
We revert this for the time being.
See GH-11428.
This commit is contained in:
nielsdos 2023-06-19 19:33:44 +02:00
parent 9f7d88802e
commit c174ebfce0
7 changed files with 10 additions and 216 deletions

2
NEWS
View File

@ -38,8 +38,6 @@ PHP NEWS
. Fix "invalid state error" with cloned namespace declarations. (nielsdos)
. Fixed bug #55294 and #47530 and #47847 (various namespace reconciliation
issues). (nielsdos)
. Fixed bug GH-11404 (DOMDocument::saveXML and friends omit xmlns=""
declaration for null namespace). (nielsdos)
. Fixed bug #80332 (Completely broken array access functionality with
DOMNamedNodeMap). (nielsdos)

View File

@ -878,10 +878,6 @@ PHP_METHOD(DOMDocument, createElementNS)
if (errorcode == 0) {
if (xmlValidateName((xmlChar *) localname, 0) == 0) {
/* https://dom.spec.whatwg.org/#validate-and-extract: demands us to set an empty string uri to NULL */
if (uri_len == 0) {
uri = NULL;
}
nodep = xmlNewDocNode(docp, NULL, (xmlChar *) localname, (xmlChar *) value);
if (nodep != NULL && uri != NULL) {
nsptr = xmlSearchNsByHref(nodep->doc, nodep, (xmlChar *) uri);

View File

@ -56,10 +56,6 @@ PHP_METHOD(DOMElement, __construct)
if (uri_len > 0) {
errorcode = dom_check_qname(name, &localname, &prefix, uri_len, name_len);
if (errorcode == 0) {
/* https://dom.spec.whatwg.org/#validate-and-extract: demands us to set an empty string uri to NULL */
if (uri_len == 0) {
uri = NULL;
}
nodep = xmlNewNode (NULL, (xmlChar *)localname);
if (nodep != NULL && uri != NULL) {
nsptr = dom_get_ns(nodep, uri, &errorcode, prefix);

View File

@ -531,6 +531,7 @@ Since: DOM Level 2
int dom_node_namespace_uri_read(dom_object *obj, zval *retval)
{
xmlNode *nodep = dom_object_get_node(obj);
char *str = NULL;
if (nodep == NULL) {
php_dom_throw_error(INVALID_STATE_ERR, 1);
@ -542,19 +543,20 @@ int dom_node_namespace_uri_read(dom_object *obj, zval *retval)
case XML_ATTRIBUTE_NODE:
case XML_NAMESPACE_DECL:
if (nodep->ns != NULL) {
char *str = (char *) nodep->ns->href;
/* https://dom.spec.whatwg.org/#concept-attribute: namespaceUri is "null or a non-empty string" */
if (str != NULL && str[0] != '\0') {
ZVAL_STRING(retval, str);
return SUCCESS;
}
str = (char *) nodep->ns->href;
}
break;
default:
str = NULL;
break;
}
if (str != NULL) {
ZVAL_STRING(retval, str);
} else {
ZVAL_NULL(retval);
}
return SUCCESS;
}

View File

@ -1496,22 +1496,6 @@ static void dom_libxml_reconcile_ensure_namespaces_are_declared(xmlNodePtr nodep
xmlDOMWrapReconcileNamespaces(&dummy_ctxt, nodep, /* options */ 0);
}
static bool dom_must_replace_namespace_by_empty_default(xmlDocPtr doc, xmlNodePtr nodep)
{
xmlNsPtr default_ns = xmlSearchNs(doc, nodep->parent, NULL);
return default_ns != NULL && default_ns->href != NULL && default_ns->href[0] != '\0';
}
static void dom_replace_namespace_by_empty_default(xmlDocPtr doc, xmlNodePtr nodep)
{
ZEND_ASSERT(nodep->ns == NULL);
/* The node uses the default empty namespace, but the current default namespace is non-empty.
* We can't unconditionally do this because otherwise libxml2 creates an xmlns="" declaration.
* Note: there's no point searching the oldNs list, because we haven't found it in the tree anyway.
* Ideally this would be pre-allocated but unfortunately libxml2 doesn't offer such a functionality. */
xmlSetNs(nodep, xmlNewNs(nodep, (const xmlChar *) "", NULL));
}
void dom_reconcile_ns(xmlDocPtr doc, xmlNodePtr nodep) /* {{{ */
{
/* Although the node type will be checked by the libxml2 API,
@ -1519,10 +1503,6 @@ void dom_reconcile_ns(xmlDocPtr doc, xmlNodePtr nodep) /* {{{ */
if (nodep->type == XML_ELEMENT_NODE) {
dom_reconcile_ns_internal(doc, nodep, nodep->parent);
dom_libxml_reconcile_ensure_namespaces_are_declared(nodep);
/* Check nodep->ns first to avoid an expensive lookup. */
if (nodep->ns == NULL && dom_must_replace_namespace_by_empty_default(doc, nodep)) {
dom_replace_namespace_by_empty_default(doc, nodep);
}
}
}
/* }}} */
@ -1546,30 +1526,12 @@ static void dom_reconcile_ns_list_internal(xmlDocPtr doc, xmlNodePtr nodep, xmlN
void dom_reconcile_ns_list(xmlDocPtr doc, xmlNodePtr nodep, xmlNodePtr last)
{
bool did_compute_must_replace_namespace_by_empty_default = false;
bool must_replace_namespace_by_empty_default = false;
dom_reconcile_ns_list_internal(doc, nodep, last, nodep->parent);
/* The loop is outside of the recursion in the above call because
* dom_libxml_reconcile_ensure_namespaces_are_declared() performs its own recursion. */
while (true) {
/* The internal libxml2 call will already check the node type, no need for us to do it here. */
dom_libxml_reconcile_ensure_namespaces_are_declared(nodep);
/* We don't have to handle the children, because if their ns's are NULL they'll just take on the default
* which should've been reconciled before. */
if (nodep->ns == NULL) {
/* This is an optimistic approach: we assume that most of the time we don't need the result of the computation. */
if (!did_compute_must_replace_namespace_by_empty_default) {
did_compute_must_replace_namespace_by_empty_default = true;
must_replace_namespace_by_empty_default = dom_must_replace_namespace_by_empty_default(doc, nodep);
}
if (must_replace_namespace_by_empty_default) {
dom_replace_namespace_by_empty_default(doc, nodep);
}
}
if (nodep == last) {
break;
}

View File

@ -121,7 +121,7 @@ test_appendChild_with_shadowing();
<html xmlns="https://php.net/something" xmlns:ns="https://php.net/whatever"><element ns:foo="https://php.net/bar"/></html>
-- Test document fragment without import --
<?xml version="1.0"?>
<html xmlns=""><element xmlns:foo="https://php.net/bar"><foo:bar/><bar/></element></html>
<html xmlns=""><element xmlns:foo="https://php.net/bar"><foo:bar/><bar xmlns=""/></element></html>
string(7) "foo:bar"
string(19) "https://php.net/bar"
-- Test document import --

View File

@ -1,160 +0,0 @@
--TEST--
GH-11404: DOMDocument::savexml and friends ommit xmlns="" declaration for null namespace, creating incorrect xml representation of the DOM
--EXTENSIONS--
dom
--FILE--
<?php
echo "-- Test append and attributes: with default namespace variation --\n";
function testAppendAndAttributes($dom) {
$nodeA = $dom->createElement('a');
$nodeB = $dom->createElementNS(null, 'b');
$nodeC = $dom->createElementNS('', 'c');
$nodeD = $dom->createElement('d');
$nodeD->setAttributeNS('some:ns', 'x:attrib', 'val');
$nodeE = $dom->createElementNS('some:ns', 'e');
// And these two respect the default ns.
$nodeE->setAttributeNS(null, 'attrib1', 'val');
$nodeE->setAttributeNS('', 'attrib2', 'val');
$dom->documentElement->appendChild($nodeA);
$dom->documentElement->appendChild($nodeB);
$dom->documentElement->appendChild($nodeC);
$dom->documentElement->appendChild($nodeD);
$dom->documentElement->appendChild($nodeE);
var_dump($nodeA->namespaceURI);
var_dump($nodeB->namespaceURI);
var_dump($nodeC->namespaceURI);
var_dump($nodeD->namespaceURI);
var_dump($nodeE->namespaceURI);
echo $dom->saveXML();
// Create a subtree without using a fragment
$subtree = $dom->createElement('subtree');
$subtree->appendChild($dom->createElementNS('some:ns', 'subtreechild1'));
$subtree->firstElementChild->appendChild($dom->createElement('subtreechild2'));
$dom->documentElement->appendChild($subtree);
echo $dom->saveXML();
// Create a subtree with the use of a fragment
$subtree = $dom->createDocumentFragment();
$subtree->appendChild($child3 = $dom->createElement('child3'));
$child3->appendChild($dom->createElement('child4'));
$subtree->appendChild($dom->createElement('child5'));
$dom->documentElement->appendChild($subtree);
echo $dom->saveXML();
}
$dom1 = new DOMDocument;
$dom1->loadXML('<?xml version="1.0" ?><with xmlns="some:ns" />');
testAppendAndAttributes($dom1);
echo "-- Test append and attributes: without default namespace variation --\n";
$dom1 = new DOMDocument;
$dom1->loadXML('<?xml version="1.0" ?><with/>');
testAppendAndAttributes($dom1);
echo "-- Test import --\n";
function testImport(?string $href, string $toBeImported) {
$dom1 = new DOMDocument;
$decl = $href === NULL ? '' : "xmlns=\"$href\"";
$dom1->loadXML('<?xml version="1.0" ?><with ' . $decl . '/>');
$dom2 = new DOMDocument;
$dom2->loadXML('<?xml version="1.0" ?>' . $toBeImported);
$dom1->documentElement->append(
$imported = $dom1->importNode($dom2->documentElement, true)
);
var_dump($imported->namespaceURI);
echo $dom1->saveXML();
}
testImport(null, '<none/>');
testImport('', '<none/>');
testImport('some:ns', '<none/>');
testImport('', '<none><div xmlns="some:ns"/></none>');
testImport('some:ns', '<none xmlns="some:ns"><div xmlns=""/></none>');
echo "-- Namespace URI comparison --\n";
$dom1 = new DOMDocument;
$dom1->loadXML('<?xml version="1.0"?><test xmlns="a:b"><div/></test>');
var_dump($dom1->firstElementChild->namespaceURI);
var_dump($dom1->firstElementChild->firstElementChild->namespaceURI);
$dom1 = new DOMDocument;
$dom1->appendChild($dom1->createElementNS('a:b', 'parent'));
$dom1->firstElementChild->appendChild($dom1->createElementNS('a:b', 'child1'));
$dom1->firstElementChild->appendChild($second = $dom1->createElement('child2'));
var_dump($dom1->firstElementChild->namespaceURI);
var_dump($dom1->firstElementChild->firstElementChild->namespaceURI);
var_dump($second->namespaceURI);
echo $dom1->saveXML();
$dom1 = new DOMDocument;
$dom1->loadXML('<?xml version="1.0"?><test xmlns="a:b"/>');
var_dump($dom1->firstElementChild->namespaceURI);
$dom1->firstElementChild->appendChild($dom1->createElementNS('a:b', 'tag'));
var_dump($dom1->firstElementChild->firstElementChild->namespaceURI);
?>
--EXPECT--
-- Test append and attributes: with default namespace variation --
NULL
NULL
NULL
NULL
string(7) "some:ns"
<?xml version="1.0"?>
<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>
<?xml version="1.0"?>
<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>
<?xml version="1.0"?>
<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>
-- Test append and attributes: without default namespace variation --
NULL
NULL
NULL
NULL
string(7) "some:ns"
<?xml version="1.0"?>
<with><a/><b/><c/><d xmlns:x="some:ns" x:attrib="val"/><e xmlns="some:ns" attrib1="val" attrib2="val"/></with>
<?xml version="1.0"?>
<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>
<?xml version="1.0"?>
<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>
-- Test import --
NULL
<?xml version="1.0"?>
<with><none/></with>
NULL
<?xml version="1.0"?>
<with xmlns=""><none/></with>
NULL
<?xml version="1.0"?>
<with xmlns="some:ns"><none xmlns=""/></with>
NULL
<?xml version="1.0"?>
<with xmlns=""><none><div xmlns="some:ns"/></none></with>
string(7) "some:ns"
<?xml version="1.0"?>
<with xmlns="some:ns"><none><div xmlns=""/></none></with>
-- Namespace URI comparison --
string(3) "a:b"
string(3) "a:b"
string(3) "a:b"
string(3) "a:b"
NULL
<?xml version="1.0"?>
<parent xmlns="a:b"><child1/><child2 xmlns=""/></parent>
string(3) "a:b"
string(3) "a:b"