Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
40c291c
Fix GH-20444: Dom\XMLDocument::C14N() seems broken compared to DOMDoc…
ndossche Nov 11, 2025
31ae40d
Merge branch 'PHP-8.4' into PHP-8.5
ndossche Dec 26, 2025
004934c
Merge branch 'PHP-8.5'
ndossche Dec 26, 2025
99ed66b
Fix GH-20582: Heap Buffer Overflow in iptcembed
ndossche Nov 25, 2025
48b4289
Merge branch 'PHP-8.3' into PHP-8.4
ndossche Dec 26, 2025
4ed8fce
Merge branch 'PHP-8.4' into PHP-8.5
ndossche Dec 26, 2025
69166c3
Merge branch 'PHP-8.5'
ndossche Dec 26, 2025
7e8c636
win32/sendmail.c: mark error messages array as const
Girgias Dec 24, 2025
4dad723
win32/sendmail.c/Post(): refactor function
Girgias Dec 24, 2025
6f28370
win32/sendmail.c: remove mailRPath parameter that is always NULL
Girgias Dec 24, 2025
0e19bc1
win32/sendmail.c: remove mailBbc parameter that is always NULL
Girgias Dec 24, 2025
4431aa9
win32/sendmail.c: remove mailCc parameter that is always NULL
Girgias Dec 24, 2025
84e63bf
win32/sendmail.c/php_win32_mail_trim_header(): use ZSTR_INIT_LITERAL()
Girgias Dec 24, 2025
f1a8944
win32/sendmail.c/SendText(): use zend_string* for headers{_lc} parame…
Girgias Dec 24, 2025
6bd9c55
win32/sendmail.c/SendText(): move string duplication code to a more l…
Girgias Dec 25, 2025
004c630
win32/sendmail.c/addToHeader(): voidify function
Girgias Dec 26, 2025
8aa64bb
win32/sendmail.c/SendText(): move posting of DATA prior to stripped h…
Girgias Dec 26, 2025
fc276ae
win32/sendmail.c/SendText(): use zend_string for stripped_headers
Girgias Dec 26, 2025
77d306e
win32/sendmail.c/PostHeader(): refactor function
Girgias Dec 26, 2025
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
121 changes: 117 additions & 4 deletions ext/dom/node.c
Original file line number Diff line number Diff line change
Expand Up @@ -2081,6 +2081,97 @@ PHP_METHOD(DOMNode, lookupNamespaceURI)
}
/* }}} end dom_node_lookup_namespace_uri */

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));
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;
} else {
node->properties = attr->next;
}
if (attr->next) {
attr->next->prev = attr->prev;
}
}
}

/* 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. */
if (node->ns && !node->ns->prefix) {
/* Workaround for the behaviour where the xmlSearchNs() call inside c14n.c
* can return the current namespace. */
zend_hash_index_add_new_ptr(links, (zend_ulong) node | 1, node->ns);
node->ns = xmlSearchNs(node->doc, node, NULL);
}
}
}

static void dom_relink_ns_decls(HashTable *links, xmlNodePtr root)
{
dom_relink_ns_decls_element(links, root);

xmlNodePtr base = root;
xmlNodePtr node = base->children;
while (node != NULL) {
dom_relink_ns_decls_element(links, node);
node = php_dom_next_in_tree_order(node, base);
}
}

static void dom_unlink_ns_decls(HashTable *links)
{
ZEND_HASH_MAP_FOREACH_NUM_KEY_VAL(links, zend_ulong h, zval *data) {
if (h & 1) {
xmlNodePtr node = (xmlNodePtr) (h ^ 1);
node->ns = Z_PTR_P(data);
} else {
xmlNodePtr node = (xmlNodePtr) h;
while (Z_LVAL_P(data)-- > 0) {
xmlNsPtr ns = node->nsDef;
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;
}

xmlFreeNs(ns);
}
}
} ZEND_HASH_FOREACH_END();
}

static int dom_canonicalize_node_parent_lookup_cb(void *user_data, xmlNodePtr node, xmlNodePtr parent)
{
xmlNodePtr root = user_data;
Expand Down Expand Up @@ -2136,7 +2227,23 @@ static void dom_canonicalization(INTERNAL_FUNCTION_PARAMETERS, int mode) /* {{{

docp = nodep->doc;

if (! docp) {
HashTable links;
bool modern = php_dom_follow_spec_node(nodep);
if (modern) {
xmlNodePtr root = nodep;
while (root->parent) {
root = root->parent;
}

if (UNEXPECTED(root->type != XML_DOCUMENT_NODE && root->type != XML_HTML_DOCUMENT_NODE)) {
php_dom_throw_error_with_message(HIERARCHY_REQUEST_ERR, "Canonicalization can only happen on nodes attached to a document.", /* strict */ true);
RETURN_THROWS();
}

zend_hash_init(&links, 0, NULL, NULL, false);
dom_relink_ns_decls(&links, xmlDocGetRootElement(docp));
} else if (!docp) {
/* Note: not triggerable with modern DOM */
zend_throw_error(NULL, "Node must be associated with a document");
RETURN_THROWS();
}
Expand All @@ -2158,12 +2265,12 @@ static void dom_canonicalization(INTERNAL_FUNCTION_PARAMETERS, int mode) /* {{{
if (!tmp) {
/* if mode == 0 then $xpath arg is 3, if mode == 1 then $xpath is 4 */
zend_argument_value_error(3 + mode, "must have a \"query\" key");
RETURN_THROWS();
goto clean_links;
}
if (Z_TYPE_P(tmp) != IS_STRING) {
/* if mode == 0 then $xpath arg is 3, if mode == 1 then $xpath is 4 */
zend_argument_type_error(3 + mode, "\"query\" option must be a string, %s given", zend_zval_value_name(tmp));
RETURN_THROWS();
goto clean_links;
}
xquery = Z_STRVAL_P(tmp);

Expand Down Expand Up @@ -2195,7 +2302,7 @@ static void dom_canonicalization(INTERNAL_FUNCTION_PARAMETERS, int mode) /* {{{
}
xmlXPathFreeContext(ctxp);
zend_throw_error(NULL, "XPath query did not return a nodeset");
RETURN_THROWS();
goto clean_links;
}
}

Expand Down Expand Up @@ -2264,6 +2371,12 @@ static void dom_canonicalization(INTERNAL_FUNCTION_PARAMETERS, int mode) /* {{{
RETURN_LONG(bytes);
}
}

clean_links:
if (modern) {
dom_unlink_ns_decls(&links);
zend_hash_destroy(&links);
}
}
/* }}} */

Expand Down
30 changes: 22 additions & 8 deletions ext/dom/tests/canonicalization.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -21,32 +21,46 @@ $dom = new DOMDocument();
$dom->loadXML($xml);
$doc = $dom->documentElement->firstChild;

$newDom = Dom\XMLDocument::createFromString($xml);
$newDoc = $newDom->documentElement->firstChild;
$counter = 0;

function check($doc, $newDoc, ...$args) {
global $counter;
$counter++;
echo $doc->C14N(...$args)."\n\n";
if ($doc->C14N(...$args) !== $newDoc->C14N(...$args)) {
var_dump($doc->C14N(...$args), $newDoc->C14N(...$args));
throw new Error("mismatch: $counter");
}
}

/* inclusive/without comments first child element of doc element is context. */
echo $doc->C14N()."\n\n";
check($doc, $newDoc);

/* exclusive/without comments first child element of doc element is context. */
echo $doc->c14N(TRUE)."\n\n";
check($doc, $newDoc, TRUE);

/* inclusive/with comments first child element of doc element is context. */
echo $doc->C14N(FALSE, TRUE)."\n\n";
check($doc, $newDoc, FALSE, TRUE);

/* exclusive/with comments first child element of doc element is context. */
echo $doc->C14N(TRUE, TRUE)."\n\n";
check($doc, $newDoc, TRUE, TRUE);

/* exclusive/without comments using xpath query. */
echo $doc->c14N(TRUE, FALSE, array('query'=>'(//. | //@* | //namespace::*)'))."\n\n";
check($doc, $newDoc, TRUE, FALSE, array('query'=>'(//. | //@* | //namespace::*)'))."\n\n";

/* exclusive/without comments first child element of doc element is context.
using xpath query with registered namespace.
test namespace prefix is also included. */
echo $doc->c14N(TRUE, FALSE,
check($doc, $newDoc, TRUE, FALSE,
array('query'=>'(//a:contain | //a:bar | .//namespace::*)',
'namespaces'=>array('a'=>'http://www.example.com/ns/foo')),
array('test'))."\n\n";
array('test'));

/* exclusive/without comments first child element of doc element is context.
test namespace prefix is also included */
echo $doc->C14N(TRUE, FALSE, NULL, array('test'));
check($doc, $newDoc, TRUE, FALSE, NULL, array('test'));
?>
--EXPECT--
<contain xmlns="http://www.example.com/ns/foo" xmlns:fubar="http://www.example.com/ns/fubar" xmlns:test="urn::test">
Expand Down
20 changes: 20 additions & 0 deletions ext/dom/tests/modern/xml/canonicalize_unattached.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
--TEST--
Canonicalize unattached node should fail
--EXTENSIONS--
dom
--FILE--
<?php

$d = \Dom\XMLDocument::createFromString('<root><child/></root>');
$child = $d->documentElement->firstChild;
$child->remove();

try {
$child->C14N();
} catch (Dom\DOMException $e) {
echo $e->getMessage(), "\n";
}

?>
--EXPECT--
Canonicalization can only happen on nodes attached to a document.
43 changes: 43 additions & 0 deletions ext/dom/tests/modern/xml/gh20444.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
--TEST--
GH-20444 (Dom\XMLDocument::C14N() seems broken compared to DOMDocument::C14N())
--EXTENSIONS--
dom
--FILE--
<?php

$xml = <<<EOF
<?xml version="1.0" encoding="UTF-8"?>
<test:root xmlns:test="http://example.com/dummy/ns">
<test:a xmlns:test="http://example.com/dummy/ns"/>
<test:b test:kind="123">abc</test:b>
</test:root>
EOF;

$d = \Dom\XMLDocument::createFromString($xml);
var_dump($d->C14N(true));

$xml = <<<EOF
<?xml version="1.0" encoding="UTF-8"?>
<ns1:root xmlns:ns1="http://example.com/dummy/ns">
<ns1:a/>
<ns1:b>
<ns1:c>123</ns1:c>
</ns1:b>
</ns1:root>
EOF;

$d = \Dom\XMLDocument::createFromString($xml);
var_dump($d->C14N());

?>
--EXPECT--
string(128) "<test:root xmlns:test="http://example.com/dummy/ns">
<test:a></test:a>
<test:b test:kind="123">abc</test:b>
</test:root>"
string(134) "<ns1:root xmlns:ns1="http://example.com/dummy/ns">
<ns1:a></ns1:a>
<ns1:b>
<ns1:c>123</ns1:c>
</ns1:b>
</ns1:root>"
Loading