Skip to content

Commit e658f80

Browse files
committed
Fix GH-12870: Creating an xmlns attribute results in a DOMException
There were multiple things here since forever, see the GH thread [1] for discussion. There were already many fixes to this function previously, and as a consequence of one of those fixes this started throwing exceptions for a correct use-case. It turns out that even when reverting to the previous behaviour there are still bugs. Just fix all of them while we have the chance. [1] #12870 Closes GH-12888.
1 parent 8524da6 commit e658f80

File tree

6 files changed

+239
-16
lines changed

6 files changed

+239
-16
lines changed

ext/dom/document.c

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -894,29 +894,61 @@ PHP_METHOD(DOMDocument, createAttributeNS)
894894
xmlNodePtr nodep = NULL, root;
895895
xmlNsPtr nsptr;
896896
int ret;
897-
size_t uri_len = 0, name_len = 0;
898-
char *uri, *name;
897+
zend_string *name, *uri;
899898
char *localname = NULL, *prefix = NULL;
900899
dom_object *intern;
901900
int errorcode;
902901

903902
id = ZEND_THIS;
904-
if (zend_parse_parameters(ZEND_NUM_ARGS(), "s!s", &uri, &uri_len, &name, &name_len) == FAILURE) {
903+
if (zend_parse_parameters(ZEND_NUM_ARGS(), "S!S", &uri, &name) == FAILURE) {
905904
RETURN_THROWS();
906905
}
907906

908907
DOM_GET_OBJ(docp, id, xmlDocPtr, intern);
909908

909+
if (UNEXPECTED(uri == NULL)) {
910+
uri = zend_empty_string;
911+
}
912+
size_t uri_len = ZSTR_LEN(uri);
913+
910914
root = xmlDocGetRootElement(docp);
911915
if (root != NULL) {
912-
errorcode = dom_check_qname(name, &localname, &prefix, uri_len, name_len);
916+
errorcode = dom_check_qname(ZSTR_VAL(name), &localname, &prefix, uri_len, ZSTR_LEN(name));
917+
/* TODO: switch to early goto-out style error-checking */
913918
if (errorcode == 0) {
914919
if (xmlValidateName((xmlChar *) localname, 0) == 0) {
920+
/* If prefix is "xml" and namespace is not the XML namespace, then throw a "NamespaceError" DOMException. */
921+
if (UNEXPECTED(!zend_string_equals_literal(uri, "http://www.w3.org/XML/1998/namespace") && xmlStrEqual(BAD_CAST prefix, BAD_CAST "xml"))) {
922+
errorcode = NAMESPACE_ERR;
923+
goto error;
924+
}
925+
/* If either qualifiedName or prefix is "xmlns" and namespace is not the XMLNS namespace, then throw a "NamespaceError" DOMException. */
926+
if (UNEXPECTED((zend_string_equals_literal(name, "xmlns") || xmlStrEqual(BAD_CAST prefix, BAD_CAST "xmlns")) && !zend_string_equals_literal(uri, "http://www.w3.org/2000/xmlns/"))) {
927+
errorcode = NAMESPACE_ERR;
928+
goto error;
929+
}
930+
/* If namespace is the XMLNS namespace and neither qualifiedName nor prefix is "xmlns", then throw a "NamespaceError" DOMException. */
931+
if (UNEXPECTED(zend_string_equals_literal(uri, "http://www.w3.org/2000/xmlns/") && !zend_string_equals_literal(name, "xmlns") && !xmlStrEqual(BAD_CAST prefix, BAD_CAST "xmlns"))) {
932+
errorcode = NAMESPACE_ERR;
933+
goto error;
934+
}
935+
915936
nodep = (xmlNodePtr) xmlNewDocProp(docp, (xmlChar *) localname, NULL);
916937
if (nodep != NULL && uri_len > 0) {
917-
nsptr = xmlSearchNsByHref(nodep->doc, root, (xmlChar *) uri);
918-
if (nsptr == NULL || nsptr->prefix == NULL) {
919-
nsptr = dom_get_ns(root, uri, &errorcode, prefix ? prefix : "default");
938+
nsptr = xmlSearchNsByHref(docp, root, BAD_CAST ZSTR_VAL(uri));
939+
940+
if (zend_string_equals_literal(name, "xmlns") || xmlStrEqual(BAD_CAST prefix, BAD_CAST "xml")) {
941+
if (nsptr == NULL) {
942+
nsptr = xmlNewNs(NULL, BAD_CAST ZSTR_VAL(uri), BAD_CAST prefix);
943+
php_libxml_set_old_ns(docp, nsptr);
944+
}
945+
} else {
946+
if (nsptr == NULL || nsptr->prefix == NULL) {
947+
nsptr = dom_get_ns_unchecked(root, ZSTR_VAL(uri), prefix ? prefix : "default");
948+
if (UNEXPECTED(nsptr == NULL)) {
949+
errorcode = NAMESPACE_ERR;
950+
}
951+
}
920952
}
921953
xmlSetNs(nodep, nsptr);
922954
}
@@ -929,6 +961,7 @@ PHP_METHOD(DOMDocument, createAttributeNS)
929961
RETURN_FALSE;
930962
}
931963

964+
error:
932965
xmlFree(localname);
933966
if (prefix != NULL) {
934967
xmlFree(prefix);

ext/dom/php_dom.c

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1596,23 +1596,30 @@ NAMESPACE_ERR: Raised if
15961596
5. the namespaceURI is "http://www.w3.org/2000/xmlns/" and neither the qualifiedName nor its prefix is "xmlns".
15971597
*/
15981598

1599+
xmlNsPtr dom_get_ns_unchecked(xmlNodePtr nodep, char *uri, char *prefix)
1600+
{
1601+
xmlNsPtr nsptr = xmlNewNs(nodep, (xmlChar *)uri, (xmlChar *)prefix);
1602+
if (UNEXPECTED(nsptr == NULL)) {
1603+
/* Either memory allocation failure, or it's because of a prefix conflict.
1604+
* We'll assume a conflict and try again. If it was a memory allocation failure we'll just fail again, whatever.
1605+
* This isn't needed for every caller (such as createElementNS & DOMElement::__construct), but isn't harmful and simplifies the mental model "when do I use which function?".
1606+
* This branch will also be taken unlikely anyway as in those cases it'll be for allocation failure. */
1607+
return dom_get_ns_resolve_prefix_conflict(nodep, uri);
1608+
}
1609+
1610+
return nsptr;
1611+
}
1612+
15991613
/* {{{ xmlNsPtr dom_get_ns(xmlNodePtr nodep, char *uri, int *errorcode, char *prefix) */
16001614
xmlNsPtr dom_get_ns(xmlNodePtr nodep, char *uri, int *errorcode, char *prefix) {
16011615
xmlNsPtr nsptr;
16021616

16031617
if (! ((prefix && !strcmp (prefix, "xml") && strcmp(uri, (char *)XML_XML_NAMESPACE)) ||
16041618
(prefix && !strcmp (prefix, "xmlns") && strcmp(uri, (char *)DOM_XMLNS_NAMESPACE)) ||
16051619
(prefix && !strcmp(uri, (char *)DOM_XMLNS_NAMESPACE) && strcmp (prefix, "xmlns")))) {
1606-
nsptr = xmlNewNs(nodep, (xmlChar *)uri, (xmlChar *)prefix);
1620+
nsptr = dom_get_ns_unchecked(nodep, uri, prefix);
16071621
if (UNEXPECTED(nsptr == NULL)) {
1608-
/* Either memory allocation failure, or it's because of a prefix conflict.
1609-
* We'll assume a conflict and try again. If it was a memory allocation failure we'll just fail again, whatever.
1610-
* This isn't needed for every caller (such as createElementNS & DOMElement::__construct), but isn't harmful and simplifies the mental model "when do I use which function?".
1611-
* This branch will also be taken unlikely anyway as in those cases it'll be for allocation failure. */
1612-
nsptr = dom_get_ns_resolve_prefix_conflict(nodep, uri);
1613-
if (UNEXPECTED(nsptr == NULL)) {
1614-
goto err;
1615-
}
1622+
goto err;
16161623
}
16171624
} else {
16181625
goto err;

ext/dom/php_dom.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ void php_dom_throw_error_with_message(int error_code, char *error_message, int s
128128
void node_list_unlink(xmlNodePtr node);
129129
int dom_check_qname(char *qname, char **localname, char **prefix, int uri_len, int name_len);
130130
xmlNsPtr dom_get_ns(xmlNodePtr node, char *uri, int *errorcode, char *prefix);
131+
xmlNsPtr dom_get_ns_unchecked(xmlNodePtr nodep, char *uri, char *prefix);
131132
void dom_reconcile_ns(xmlDocPtr doc, xmlNodePtr nodep);
132133
void dom_reconcile_ns_list(xmlDocPtr doc, xmlNodePtr nodep, xmlNodePtr last);
133134
xmlNsPtr dom_get_nsdecl(xmlNode *node, xmlChar *localName);

ext/dom/tests/gh12870.inc

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
function test(?string $uri, string $qualifiedName) {
4+
$uri_readable = is_null($uri) ? 'NULL' : "\"$uri\"";
5+
echo "--- Testing $uri_readable, \"$qualifiedName\" ---\n";
6+
$d = new DOMDocument();
7+
$d->appendChild($d->createElement('root'));
8+
try {
9+
$attr = $d->createAttributeNS($uri, $qualifiedName);
10+
$d->documentElement->setAttributeNodeNS($attr);
11+
echo "Attr prefix: ";
12+
var_dump($attr->prefix);
13+
echo "Attr namespaceURI: ";
14+
var_dump($attr->namespaceURI);
15+
echo "Attr value: ";
16+
var_dump($attr->value);
17+
echo "root namespaceURI: ";
18+
var_dump($d->documentElement->namespaceURI);
19+
echo "Equality check: ";
20+
$parts = explode(':', $qualifiedName);
21+
var_dump($attr === $d->documentElement->getAttributeNodeNS($uri, $parts[count($parts) - 1]));
22+
echo $d->saveXML(), "\n";
23+
} catch (DOMException $e) {
24+
echo "Exception: ", $e->getMessage(), "\n\n";
25+
}
26+
}

ext/dom/tests/gh12870_a.phpt

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
--TEST--
2+
GH-12870 (Creating an xmlns attribute results in a DOMException) - xmlns variations
3+
--EXTENSIONS--
4+
dom
5+
--FILE--
6+
<?php
7+
8+
require __DIR__ . '/gh12870.inc';
9+
10+
echo "=== NORMAL CASES ===\n";
11+
12+
test('http://www.w3.org/2000/xmlns/qx', 'foo:xmlns');
13+
test('http://www.w3.org/2000/xmlns/', 'xmlns');
14+
test('http://www.w3.org/2000/xmlns/', 'xmlns:xmlns');
15+
16+
echo "=== ERROR CASES ===\n";
17+
18+
test('http://www.w3.org/2000/xmlns/', 'bar:xmlns');
19+
test('http://www.w3.org/2000/xmlns/a', 'xmlns');
20+
test('http://www.w3.org/2000/xmlns/a', 'xmlns:bar');
21+
test(null, 'xmlns:bar');
22+
test('', 'xmlns');
23+
test('http://www.w3.org/2000/xmlns/', '');
24+
25+
?>
26+
--EXPECT--
27+
=== NORMAL CASES ===
28+
--- Testing "http://www.w3.org/2000/xmlns/qx", "foo:xmlns" ---
29+
Attr prefix: string(3) "foo"
30+
Attr namespaceURI: string(31) "http://www.w3.org/2000/xmlns/qx"
31+
Attr value: string(0) ""
32+
root namespaceURI: NULL
33+
Equality check: bool(true)
34+
<?xml version="1.0"?>
35+
<root xmlns:foo="http://www.w3.org/2000/xmlns/qx" foo:xmlns=""/>
36+
37+
--- Testing "http://www.w3.org/2000/xmlns/", "xmlns" ---
38+
Attr prefix: string(0) ""
39+
Attr namespaceURI: string(29) "http://www.w3.org/2000/xmlns/"
40+
Attr value: string(0) ""
41+
root namespaceURI: NULL
42+
Equality check: bool(true)
43+
<?xml version="1.0"?>
44+
<root xmlns=""/>
45+
46+
--- Testing "http://www.w3.org/2000/xmlns/", "xmlns:xmlns" ---
47+
Attr prefix: string(5) "xmlns"
48+
Attr namespaceURI: string(29) "http://www.w3.org/2000/xmlns/"
49+
Attr value: string(0) ""
50+
root namespaceURI: NULL
51+
Equality check: bool(true)
52+
<?xml version="1.0"?>
53+
<root xmlns:xmlns="http://www.w3.org/2000/xmlns/" xmlns:xmlns=""/>
54+
55+
=== ERROR CASES ===
56+
--- Testing "http://www.w3.org/2000/xmlns/", "bar:xmlns" ---
57+
Exception: Namespace Error
58+
59+
--- Testing "http://www.w3.org/2000/xmlns/a", "xmlns" ---
60+
Exception: Namespace Error
61+
62+
--- Testing "http://www.w3.org/2000/xmlns/a", "xmlns:bar" ---
63+
Exception: Namespace Error
64+
65+
--- Testing NULL, "xmlns:bar" ---
66+
Exception: Namespace Error
67+
68+
--- Testing "", "xmlns" ---
69+
Exception: Namespace Error
70+
71+
--- Testing "http://www.w3.org/2000/xmlns/", "" ---
72+
Exception: Namespace Error

ext/dom/tests/gh12870_b.phpt

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
--TEST--
2+
GH-12870 (Creating an xmlns attribute results in a DOMException) - xml variations
3+
--EXTENSIONS--
4+
dom
5+
--FILE--
6+
<?php
7+
8+
require __DIR__ . '/gh12870.inc';
9+
10+
echo "=== NORMAL CASES ===\n";
11+
12+
test('http://www.w3.org/XML/1998/namespaceqx', 'foo:xml');
13+
test('http://www.w3.org/XML/1998/namespace', 'xml');
14+
test('http://www.w3.org/XML/1998/namespace', 'bar:xml');
15+
test('', 'xml');
16+
test('http://www.w3.org/XML/1998/namespacea', 'xml');
17+
18+
echo "=== ERROR CASES ===\n";
19+
20+
test('http://www.w3.org/XML/1998/namespace', 'xmlns:xml');
21+
test('http://www.w3.org/XML/1998/namespace', '');
22+
test('http://www.w3.org/XML/1998/namespacea', 'xml:foo');
23+
test(NULL, 'xml:foo');
24+
25+
?>
26+
--EXPECT--
27+
=== NORMAL CASES ===
28+
--- Testing "http://www.w3.org/XML/1998/namespaceqx", "foo:xml" ---
29+
Attr prefix: string(3) "foo"
30+
Attr namespaceURI: string(38) "http://www.w3.org/XML/1998/namespaceqx"
31+
Attr value: string(0) ""
32+
root namespaceURI: NULL
33+
Equality check: bool(true)
34+
<?xml version="1.0"?>
35+
<root xmlns:foo="http://www.w3.org/XML/1998/namespaceqx" foo:xml=""/>
36+
37+
--- Testing "http://www.w3.org/XML/1998/namespace", "xml" ---
38+
Attr prefix: string(3) "xml"
39+
Attr namespaceURI: string(36) "http://www.w3.org/XML/1998/namespace"
40+
Attr value: string(0) ""
41+
root namespaceURI: NULL
42+
Equality check: bool(true)
43+
<?xml version="1.0"?>
44+
<root xml:xml=""/>
45+
46+
--- Testing "http://www.w3.org/XML/1998/namespace", "bar:xml" ---
47+
Attr prefix: string(3) "xml"
48+
Attr namespaceURI: string(36) "http://www.w3.org/XML/1998/namespace"
49+
Attr value: string(0) ""
50+
root namespaceURI: NULL
51+
Equality check: bool(true)
52+
<?xml version="1.0"?>
53+
<root xml:xml=""/>
54+
55+
--- Testing "", "xml" ---
56+
Attr prefix: string(0) ""
57+
Attr namespaceURI: NULL
58+
Attr value: string(0) ""
59+
root namespaceURI: NULL
60+
Equality check: bool(false)
61+
<?xml version="1.0"?>
62+
<root xml=""/>
63+
64+
--- Testing "http://www.w3.org/XML/1998/namespacea", "xml" ---
65+
Attr prefix: string(7) "default"
66+
Attr namespaceURI: string(37) "http://www.w3.org/XML/1998/namespacea"
67+
Attr value: string(0) ""
68+
root namespaceURI: NULL
69+
Equality check: bool(true)
70+
<?xml version="1.0"?>
71+
<root xmlns:default="http://www.w3.org/XML/1998/namespacea" default:xml=""/>
72+
73+
=== ERROR CASES ===
74+
--- Testing "http://www.w3.org/XML/1998/namespace", "xmlns:xml" ---
75+
Exception: Namespace Error
76+
77+
--- Testing "http://www.w3.org/XML/1998/namespace", "" ---
78+
Exception: Namespace Error
79+
80+
--- Testing "http://www.w3.org/XML/1998/namespacea", "xml:foo" ---
81+
Exception: Namespace Error
82+
83+
--- Testing NULL, "xml:foo" ---
84+
Exception: Namespace Error

0 commit comments

Comments
 (0)