Skip to content

Commit 284fd77

Browse files
committed
Fix GH-21357: XSLTProcessor works with DOMDocument, but fails with Dom\XMLDocument
Registering namespace after the parsing is too late because parsing can fail due to attributes referencing namespaces. So we have to register fake namespaces before the parsing. However, the clone operation reconciles namespaces in the wrong way, so we have to clone via an object. Closes GH-21371.
1 parent 80dc4c1 commit 284fd77

File tree

5 files changed

+115
-75
lines changed

5 files changed

+115
-75
lines changed

NEWS

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ PHP NEWS
3030
- Sysvshm:
3131
. Fix memory leak in shm_get_var() when variable is corrupted. (ndossche)
3232

33+
- XSL:
34+
. Fix GH-21357 (XSLTProcessor works with DOMDocument, but fails with
35+
Dom\XMLDocument). (ndossche)
36+
3337
12 Mar 2026, PHP 8.4.19
3438

3539
- Core:

ext/xsl/tests/auto_registration_namespaces_new_dom.phpt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ $sheet = Dom\XMLDocument::createFromString(<<<XML
2424
XML);
2525

2626
// Make sure it will auto-register urn:test
27-
$sheet->documentElement->append($sheet->createElementNS('urn:test', 'test:dummy'));
27+
$sheet->documentElement->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:test', 'urn:test');
2828

2929
$input = Dom\XMLDocument::createFromString(<<<XML
3030
<root>

ext/xsl/tests/gh21357_1.phpt

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
--TEST--
2+
GH-21357 (XSLTProcessor works with \DOMDocument, but fails with \Dom\XMLDocument)
3+
--EXTENSIONS--
4+
dom
5+
xsl
6+
--CREDITS--
7+
jacekkow
8+
--FILE--
9+
<?php
10+
$xml = <<<'XML'
11+
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:tns="urn:myns" version="1.0">
12+
<xsl:template match="tns:referee"/>
13+
</xsl:stylesheet>
14+
XML;
15+
16+
$dom = Dom\XMLDocument::createFromString($xml);
17+
var_dump(new XSLTProcessor()->importStylesheet($dom));
18+
?>
19+
--EXPECT--
20+
bool(true)

ext/xsl/tests/gh21357_2.phpt

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
--TEST--
2+
GH-21357 (XSLTProcessor works with \DOMDocument, but fails with \Dom\XMLDocument)
3+
--EXTENSIONS--
4+
dom
5+
xsl
6+
--CREDITS--
7+
jacekkow
8+
--FILE--
9+
<?php
10+
$xsl = '<?xml version="1.0" encoding="utf-8"?>
11+
<xsl:stylesheet
12+
version="1.0"
13+
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
14+
xmlns:old="http://something/old/"
15+
xmlns:new="http://something/new/">
16+
<xsl:template match="node()|@*">
17+
<xsl:copy>
18+
<xsl:apply-templates select="node()|@*"/>
19+
</xsl:copy>
20+
</xsl:template>
21+
<xsl:template match="old:*">
22+
<xsl:element name="{local-name()}" xmlns="http://something/new/" >
23+
<xsl:apply-templates select="node()|@*"/>
24+
</xsl:element>
25+
</xsl:template>
26+
</xsl:stylesheet>';
27+
$dom = Dom\XMLDocument::createFromString($xsl);
28+
$xsl = new XSLTProcessor();
29+
$xsl->importStylesheet($dom);
30+
var_dump($xsl->transformToXml(\Dom\XMLDocument::createFromString('<test xmlns="http://something/old/"/>')));
31+
?>
32+
--EXPECT--
33+
string(138) "<?xml version="1.0"?>
34+
<test xmlns="http://something/new/" xmlns:ns_1="http://www.w3.org/2000/xmlns/" ns_1:xmlns="http://something/old/"/>
35+
"

ext/xsl/xsltprocessor.c

Lines changed: 55 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -123,65 +123,38 @@ static void xsl_ext_function_trampoline(xmlXPathParserContextPtr ctxt, int nargs
123123
}
124124
}
125125

126-
static void xsl_add_ns_to_map(xmlHashTablePtr table, xsltStylesheetPtr sheet, const xmlNode *cur, const xmlChar *prefix, const xmlChar *uri)
126+
static void xsl_add_ns_def(xmlNodePtr node)
127127
{
128-
const xmlChar *existing_url = xmlHashLookup(table, prefix);
129-
if (existing_url != NULL && !xmlStrEqual(existing_url, uri)) {
130-
xsltTransformError(NULL, sheet, (xmlNodePtr) cur, "Namespaces prefix %s used for multiple namespaces\n", prefix);
131-
sheet->warnings++;
132-
} else if (existing_url == NULL) {
133-
xmlHashUpdateEntry(table, prefix, (void *) uri, NULL);
134-
}
135-
}
136-
137-
/* Adds all namespace declaration (not using nsDef) into a hash map that maps prefix to uri. Warns on conflicting declarations. */
138-
static void xsl_build_ns_map(xmlHashTablePtr table, xsltStylesheetPtr sheet, php_dom_libxml_ns_mapper *ns_mapper, const xmlDoc *doc)
139-
{
140-
const xmlNode *cur = xmlDocGetRootElement(doc);
128+
if (node->type == XML_ELEMENT_NODE) {
129+
for (xmlAttrPtr attr = node->properties; attr; attr = attr->next) {
130+
if (php_dom_ns_is_fast((const xmlNode *) attr, php_dom_ns_is_xmlns_magic_token)) {
131+
xmlNsPtr ns = xmlMalloc(sizeof(*ns));
132+
if (!ns) {
133+
return;
134+
}
141135

142-
while (cur != NULL) {
143-
if (cur->type == XML_ELEMENT_NODE) {
144-
if (cur->ns != NULL && cur->ns->prefix != NULL) {
145-
xsl_add_ns_to_map(table, sheet, cur, cur->ns->prefix, cur->ns->href);
146-
}
136+
bool should_free;
137+
xmlChar *attr_value = php_libxml_attr_value(attr, &should_free);
147138

148-
for (const xmlAttr *attr = cur->properties; attr != NULL; attr = attr->next) {
149-
if (attr->ns != NULL && attr->ns->prefix != NULL && php_dom_ns_is_fast_ex(attr->ns, php_dom_ns_is_xmlns_magic_token)
150-
&& attr->children != NULL && attr->children->content != NULL) {
151-
/* This attribute declares a namespace, get the relevant instance.
152-
* The declared namespace is not the same as the namespace of this attribute (which is xmlns). */
153-
const xmlChar *prefix = attr->name;
154-
xmlNsPtr ns = php_dom_libxml_ns_mapper_get_ns_raw_strings_nullsafe(ns_mapper, (const char *) prefix, (const char *) attr->children->content);
155-
xsl_add_ns_to_map(table, sheet, cur, prefix, ns->href);
156-
}
139+
memset(ns, 0, sizeof(*ns));
140+
ns->type = XML_LOCAL_NAMESPACE;
141+
ns->href = should_free ? attr_value : xmlStrdup(attr_value);
142+
ns->prefix = attr->ns->prefix ? xmlStrdup(attr->name) : NULL;
143+
ns->next = node->nsDef;
144+
node->nsDef = ns;
157145
}
158146
}
159-
160-
cur = php_dom_next_in_tree_order(cur, (const xmlNode *) doc);
161147
}
162148
}
163149

164-
/* Apply namespace corrections for new DOM */
165-
typedef enum {
166-
XSL_NS_HASH_CORRECTION_NONE = 0,
167-
XSL_NS_HASH_CORRECTION_APPLIED = 1,
168-
XSL_NS_HASH_CORRECTION_FAILED = 2
169-
} xsl_ns_hash_correction_status;
170-
171-
static zend_always_inline xsl_ns_hash_correction_status xsl_apply_ns_hash_corrections(xsltStylesheetPtr sheetp, xmlNodePtr nodep, xmlDocPtr doc)
150+
static void xsl_add_ns_defs(xmlDocPtr doc)
172151
{
173-
if (sheetp->nsHash == NULL) {
174-
dom_object *node_intern = php_dom_object_get_data(nodep);
175-
if (node_intern != NULL && php_dom_follow_spec_intern(node_intern)) {
176-
sheetp->nsHash = xmlHashCreate(10);
177-
if (UNEXPECTED(!sheetp->nsHash)) {
178-
return XSL_NS_HASH_CORRECTION_FAILED;
179-
}
180-
xsl_build_ns_map(sheetp->nsHash, sheetp, php_dom_get_ns_mapper(node_intern), doc);
181-
return XSL_NS_HASH_CORRECTION_APPLIED;
182-
}
152+
xmlNodePtr base = (xmlNodePtr) doc;
153+
xmlNodePtr node = base->children;
154+
while (node != NULL) {
155+
xsl_add_ns_def(node);
156+
node = php_dom_next_in_tree_order(node, base);
183157
}
184-
return XSL_NS_HASH_CORRECTION_NONE;
185158
}
186159

187160
/* {{{ URL: http://www.w3.org/TR/2003/WD-DOM-Level-3-Core-20030226/DOM3-Core.html#
@@ -190,63 +163,71 @@ static zend_always_inline xsl_ns_hash_correction_status xsl_apply_ns_hash_correc
190163
PHP_METHOD(XSLTProcessor, importStylesheet)
191164
{
192165
zval *id, *docp = NULL;
193-
xmlDoc *doc = NULL, *newdoc = NULL;
166+
xmlDoc *newdoc = NULL;
194167
xsltStylesheetPtr sheetp;
195168
bool clone_docu = false;
196169
xmlNode *nodep = NULL;
197-
zval *cloneDocu, rv;
170+
zval *cloneDocu, rv, clone_zv;
198171
zend_string *member;
199172

200173
id = ZEND_THIS;
201174
if (zend_parse_parameters(ZEND_NUM_ARGS(), "o", &docp) == FAILURE) {
202175
RETURN_THROWS();
203176
}
204177

205-
nodep = php_libxml_import_node(docp);
178+
/* libxslt uses _private, so we must copy the imported
179+
* stylesheet document otherwise the node proxies will be a mess.
180+
* We will clone the object and detach the libxml internals later. */
181+
zend_object *clone = Z_OBJ_HANDLER_P(docp, clone_obj)(Z_OBJ_P(docp));
182+
if (!clone) {
183+
RETURN_THROWS();
184+
}
185+
186+
ZVAL_OBJ(&clone_zv, clone);
187+
nodep = php_libxml_import_node(&clone_zv);
206188

207189
if (nodep) {
208-
doc = nodep->doc;
190+
newdoc = nodep->doc;
209191
}
210-
if (doc == NULL) {
192+
if (newdoc == NULL) {
193+
OBJ_RELEASE(clone);
211194
zend_argument_type_error(1, "must be a valid XML node");
212195
RETURN_THROWS();
213196
}
214197

215-
/* libxslt uses _private, so we must copy the imported
216-
stylesheet document otherwise the node proxies will be a mess */
217-
newdoc = xmlCopyDoc(doc, 1);
218-
xmlNodeSetBase((xmlNodePtr) newdoc, (xmlChar *)doc->URL);
198+
php_libxml_node_object *clone_lxml_obj = Z_LIBXML_NODE_P(&clone_zv);
199+
219200
PHP_LIBXML_SANITIZE_GLOBALS(parse);
220201
ZEND_DIAGNOSTIC_IGNORED_START("-Wdeprecated-declarations")
221202
xmlSubstituteEntitiesDefault(1);
222203
xmlLoadExtDtdDefaultValue = XML_DETECT_IDS | XML_COMPLETE_ATTRS;
223204
ZEND_DIAGNOSTIC_IGNORED_END
224205

206+
if (clone_lxml_obj->document->class_type == PHP_LIBXML_CLASS_MODERN) {
207+
xsl_add_ns_defs(newdoc);
208+
}
209+
225210
sheetp = xsltParseStylesheetDoc(newdoc);
226211
PHP_LIBXML_RESTORE_GLOBALS(parse);
227212

228213
if (!sheetp) {
229-
xmlFreeDoc(newdoc);
214+
OBJ_RELEASE(clone);
230215
RETURN_FALSE;
231216
}
232217

233218
xsl_object *intern = Z_XSL_P(id);
234219

235-
xsl_ns_hash_correction_status status = xsl_apply_ns_hash_corrections(sheetp, nodep, doc);
236-
if (UNEXPECTED(status == XSL_NS_HASH_CORRECTION_FAILED)) {
237-
xsltFreeStylesheet(sheetp);
238-
xmlFreeDoc(newdoc);
239-
RETURN_FALSE;
240-
} else if (status == XSL_NS_HASH_CORRECTION_APPLIED) {
241-
/* The namespace mappings need to be kept alive.
242-
* This is stored in the ref obj outside of libxml2, but that means that the sheet won't keep it alive
243-
* unlike with namespaces from old DOM. */
244-
if (intern->sheet_ref_obj) {
245-
php_libxml_decrement_doc_ref_directly(intern->sheet_ref_obj);
246-
}
247-
intern->sheet_ref_obj = Z_LIBXML_NODE_P(docp)->document;
248-
intern->sheet_ref_obj->refcount++;
249-
}
220+
/* Detach object */
221+
clone_lxml_obj->document->ptr = NULL;
222+
/* The namespace mappings need to be kept alive.
223+
* This is stored in the ref obj outside of libxml2, but that means that the sheet won't keep it alive
224+
* unlike with namespaces from old DOM. */
225+
if (intern->sheet_ref_obj) {
226+
php_libxml_decrement_doc_ref_directly(intern->sheet_ref_obj);
227+
}
228+
intern->sheet_ref_obj = clone_lxml_obj->document;
229+
intern->sheet_ref_obj->refcount++;
230+
OBJ_RELEASE(clone);
250231

251232
member = ZSTR_INIT_LITERAL("cloneDocument", 0);
252233
cloneDocu = zend_std_read_property(Z_OBJ_P(id), member, BP_VAR_R, NULL, &rv);

0 commit comments

Comments
 (0)