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
90 changes: 70 additions & 20 deletions ext/dom/node.c
Original file line number Diff line number Diff line change
Expand Up @@ -2103,33 +2103,72 @@ 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;
}

/* Mint a temporary nsDef entry so C14N finds namespaces that live on node->ns
* but have no matching xmlns attribute (typical for createElementNS). */
static void dom_add_synthetic_ns_decl(HashTable *links, xmlNodePtr node, xmlNsPtr src_ns)
{
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;
}

/* Same, but for attribute namespaces, which may collide by prefix with the
* element's own ns or with a sibling attribute's ns. */
static void dom_add_synthetic_ns_decl_for_attr(HashTable *links, xmlNodePtr node, xmlNsPtr src_ns)
{
for (xmlNsPtr existing = node->nsDef; existing; existing = existing->next) {
if (xmlStrEqual(existing->prefix, src_ns->prefix)) {
return;
}
}

dom_add_synthetic_ns_decl(links, node, src_ns);
}

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 +2181,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_for_attr(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 +2227,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