Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 69 additions & 20 deletions ext/dom/node.c
Original file line number Diff line number Diff line change
Expand Up @@ -2103,33 +2103,71 @@ PHP_METHOD(DOMNode, lookupNamespaceURI)
}
/* }}} end dom_node_lookup_namespace_uri */

/* Allocate, track and prepend a temporary nsDef entry for C14N.
* Returns the new xmlNsPtr for the caller to fill in href/prefix/_private,
* or NULL on allocation failure. */
static xmlNsPtr dom_alloc_ns_decl(HashTable *links, xmlNodePtr node)
{
xmlNsPtr ns = xmlMalloc(sizeof(*ns));
if (!ns) {
return NULL;
}

zval *zv = zend_hash_index_lookup(links, (zend_ulong) node);
if (Z_ISNULL_P(zv)) {
ZVAL_LONG(zv, 1);
} else {
Z_LVAL_P(zv)++;
}

memset(ns, 0, sizeof(*ns));
ns->type = XML_LOCAL_NAMESPACE;
ns->next = node->nsDef;
node->nsDef = ns;

return ns;
}

/* For DOM-built documents (e.g. createElementNS), namespaces live on node->ns
* without corresponding xmlns attributes. C14N expects nsDef entries, so create
* temporary ones. The _private field stays NULL to distinguish from attr-based
* entries during cleanup. */
static void dom_add_synthetic_ns_decl(HashTable *links, xmlNodePtr node, xmlNsPtr src_ns)
{
/* Pre-existing nsDef entries are never filled in for Dom\XMLDocument.
* This loop guards against duplicates from our own synthetic additions
* within the same C14N pass (e.g. when an element and its attributes
* share the same namespace prefix). */
for (xmlNsPtr existing = node->nsDef; existing; existing = existing->next) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this loop? I believe that for Dom\XMLDocument this is normally never filled in.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The loop checks existing nsDef entries to avoid duplicates. While pre-existing nsDef is indeed empty for Dom\XMLDocument, the loop is still needed:
dom_add_synthetic_ns_decl is called multiple times per node (once for node->ns, once per attr->ns), and if the element and an attribute share the same namespace
prefix, the loop prevents adding a duplicate synthetic entry.

if (xmlStrEqual(existing->prefix, src_ns->prefix)) {
return;
}
}

xmlNsPtr ns = dom_alloc_ns_decl(links, node);
if (!ns) {
return;
}

ns->href = xmlStrdup(src_ns->href);
ns->prefix = src_ns->prefix ? xmlStrdup(src_ns->prefix) : NULL;
}

static void dom_relink_ns_decls_element(HashTable *links, xmlNodePtr node)
{
if (node->type == XML_ELEMENT_NODE) {
for (xmlAttrPtr attr = node->properties; attr; attr = attr->next) {
if (php_dom_ns_is_fast((const xmlNode *) attr, php_dom_ns_is_xmlns_magic_token)) {
xmlNsPtr ns = xmlMalloc(sizeof(*ns));
xmlNsPtr ns = dom_alloc_ns_decl(links, node);
if (!ns) {
return;
}

zval *zv = zend_hash_index_lookup(links, (zend_ulong) node);
if (Z_ISNULL_P(zv)) {
ZVAL_LONG(zv, 1);
} else {
Z_LVAL_P(zv)++;
}

bool should_free;
xmlChar *attr_value = php_libxml_attr_value(attr, &should_free);

memset(ns, 0, sizeof(*ns));
ns->type = XML_LOCAL_NAMESPACE;
ns->href = should_free ? attr_value : xmlStrdup(attr_value);
ns->prefix = attr->ns->prefix ? xmlStrdup(attr->name) : NULL;
ns->next = node->nsDef;
node->nsDef = ns;

ns->_private = attr;
if (attr->prev) {
attr->prev = attr->next;
Expand All @@ -2142,6 +2180,15 @@ static void dom_relink_ns_decls_element(HashTable *links, xmlNodePtr node)
}
}

if (node->ns) {
dom_add_synthetic_ns_decl(links, node, node->ns);
}
for (xmlAttrPtr attr = node->properties; attr; attr = attr->next) {
if (attr->ns && !php_dom_ns_is_fast((const xmlNode *) attr, php_dom_ns_is_xmlns_magic_token)) {
dom_add_synthetic_ns_decl(links, node, attr->ns);
}
}

/* The default namespace is handled separately from the other namespaces in C14N.
* The default namespace is explicitly looked up while the other namespaces are
* deduplicated and compared to a list of visible namespaces. */
Expand Down Expand Up @@ -2179,13 +2226,15 @@ static void dom_unlink_ns_decls(HashTable *links)
node->nsDef = ns->next;

xmlAttrPtr attr = ns->_private;
if (attr->prev) {
attr->prev->next = attr;
} else {
node->properties = attr;
}
if (attr->next) {
attr->next->prev = attr;
if (attr) {
if (attr->prev) {
attr->prev->next = attr;
} else {
node->properties = attr;
}
if (attr->next) {
attr->next->prev = attr;
}
}

xmlFreeNs(ns);
Expand Down
28 changes: 28 additions & 0 deletions ext/dom/tests/modern/xml/gh21544.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
--TEST--
GH-21544 (Dom\XMLDocument::C14N() drops namespace declarations on DOM-built documents)
--CREDITS--
Toon Verwerft (veewee)
--EXTENSIONS--
dom
--FILE--
<?php

$doc = Dom\XMLDocument::createEmpty();
$root = $doc->createElementNS("urn:envelope", "env:Root");
$doc->appendChild($root);
$child = $doc->createElementNS("urn:child", "x:Child");
$root->appendChild($child);

$parsed = Dom\XMLDocument::createFromString(
'<env:Root xmlns:env="urn:envelope"><x:Child xmlns:x="urn:child"/></env:Root>'
);

echo "DOM-built C14N: " . $doc->C14N() . PHP_EOL;
echo "Parsed C14N: " . $parsed->C14N() . PHP_EOL;
var_dump($doc->C14N() === $parsed->C14N());

?>
--EXPECT--
DOM-built C14N: <env:Root xmlns:env="urn:envelope"><x:Child xmlns:x="urn:child"></x:Child></env:Root>
Parsed C14N: <env:Root xmlns:env="urn:envelope"><x:Child xmlns:x="urn:child"></x:Child></env:Root>
bool(true)
Loading