diff --git a/ext/dom/document.c b/ext/dom/document.c index bfb06d7e4cca4..f254d87e7d63c 100644 --- a/ext/dom/document.c +++ b/ext/dom/document.c @@ -805,7 +805,7 @@ PHP_METHOD(DOMDocument, importNode) xmlNodePtr root = xmlDocGetRootElement(docp); nsptr = xmlSearchNsByHref (nodep->doc, root, nodep->ns->href); - if (nsptr == NULL) { + if (nsptr == NULL || nsptr->prefix == NULL) { int errorcode; nsptr = dom_get_ns(root, (char *) nodep->ns->href, &errorcode, (char *) nodep->ns->prefix); } @@ -910,8 +910,8 @@ PHP_METHOD(DOMDocument, createAttributeNS) nodep = (xmlNodePtr) xmlNewDocProp(docp, (xmlChar *) localname, NULL); if (nodep != NULL && uri_len > 0) { nsptr = xmlSearchNsByHref(nodep->doc, root, (xmlChar *) uri); - if (nsptr == NULL) { - nsptr = dom_get_ns(root, uri, &errorcode, prefix); + if (nsptr == NULL || nsptr->prefix == NULL) { + nsptr = dom_get_ns(root, uri, &errorcode, prefix ? prefix : "default"); } xmlSetNs(nodep, nsptr); } diff --git a/ext/dom/element.c b/ext/dom/element.c index 8fb19d9e2eaed..fabc3c514bf96 100644 --- a/ext/dom/element.c +++ b/ext/dom/element.c @@ -655,45 +655,6 @@ PHP_METHOD(DOMElement, getAttributeNS) } /* }}} end dom_element_get_attribute_ns */ -static xmlNsPtr _dom_new_reconNs(xmlDocPtr doc, xmlNodePtr tree, xmlNsPtr ns) /* {{{ */ -{ - xmlNsPtr def; - xmlChar prefix[50]; - int counter = 1; - - if ((tree == NULL) || (ns == NULL) || (ns->type != XML_NAMESPACE_DECL)) { - return NULL; - } - - /* Code taken from libxml2 (2.6.20) xmlNewReconciliedNs - * - * Find a close prefix which is not already in use. - * Let's strip namespace prefixes longer than 20 chars ! - */ - if (ns->prefix == NULL) - snprintf((char *) prefix, sizeof(prefix), "default"); - else - snprintf((char *) prefix, sizeof(prefix), "%.20s", (char *)ns->prefix); - - def = xmlSearchNs(doc, tree, prefix); - while (def != NULL) { - if (counter > 1000) return(NULL); - if (ns->prefix == NULL) - snprintf((char *) prefix, sizeof(prefix), "default%d", counter++); - else - snprintf((char *) prefix, sizeof(prefix), "%.20s%d", - (char *)ns->prefix, counter++); - def = xmlSearchNs(doc, tree, prefix); - } - - /* - * OK, now we are ready to create a new one. - */ - def = xmlNewNs(tree, ns->href, prefix); - return(def); -} -/* }}} */ - /* {{{ URL: http://www.w3.org/TR/2003/WD-DOM-Level-3-Core-20030226/DOM3-Core.html#core-ID-ElSetAttrNS Since: DOM Level 2 */ @@ -756,27 +717,18 @@ PHP_METHOD(DOMElement, setAttributeNS) tmpnsptr = tmpnsptr->next; } if (tmpnsptr == NULL) { - nsptr = _dom_new_reconNs(elemp->doc, elemp, nsptr); + nsptr = dom_get_ns_resolve_prefix_conflict(elemp, (const char *) nsptr->href); } } } if (nsptr == NULL) { - if (prefix == NULL) { - if (is_xmlns == 1) { - xmlNewNs(elemp, (xmlChar *)value, NULL); - xmlReconciliateNs(elemp->doc, elemp); - } else { - errorcode = NAMESPACE_ERR; - } + if (is_xmlns == 1) { + xmlNewNs(elemp, (xmlChar *)value, prefix == NULL ? NULL : (xmlChar *)localname); } else { - if (is_xmlns == 1) { - xmlNewNs(elemp, (xmlChar *)value, (xmlChar *)localname); - } else { - nsptr = dom_get_ns(elemp, uri, &errorcode, prefix); - } - xmlReconciliateNs(elemp->doc, elemp); + nsptr = dom_get_ns(elemp, uri, &errorcode, prefix); } + xmlReconciliateNs(elemp->doc, elemp); } else { if (is_xmlns == 1) { if (nsptr->href) { diff --git a/ext/dom/php_dom.c b/ext/dom/php_dom.c index 9e036214bde53..bada817e301e8 100644 --- a/ext/dom/php_dom.c +++ b/ext/dom/php_dom.c @@ -32,10 +32,6 @@ #define PHP_XPATH 1 #define PHP_XPTR 2 -/* libxml2 doesn't expose this constant as part of their public API. - * See xmlDOMReconcileNSOptions in tree.c */ -#define PHP_LIBXML2_DOM_RECONNS_REMOVEREDUND (1 << 0) - /* {{{ class entries */ PHP_DOM_EXPORT zend_class_entry *dom_node_class_entry; PHP_DOM_EXPORT zend_class_entry *dom_domexception_class_entry; @@ -1473,8 +1469,7 @@ static void dom_libxml_reconcile_ensure_namespaces_are_declared(xmlNodePtr nodep * Although libxml2 currently does not use this for the reconciliation, it still * makes sense to do this just in case libxml2's internal change in the future. */ xmlDOMWrapCtxt dummy_ctxt = {0}; - bool remove_redundant = nodep->nsDef == NULL && nodep->ns != NULL; - xmlDOMWrapReconcileNamespaces(&dummy_ctxt, nodep, /* options */ remove_redundant ? PHP_LIBXML2_DOM_RECONNS_REMOVEREDUND : 0); + xmlDOMWrapReconcileNamespaces(&dummy_ctxt, nodep, /* options */ 0); } void dom_reconcile_ns(xmlDocPtr doc, xmlNodePtr nodep) /* {{{ */ @@ -1557,6 +1552,35 @@ int dom_check_qname(char *qname, char **localname, char **prefix, int uri_len, i } /* }}} */ +/* Creates a new namespace declaration with a random prefix with the given uri on the tree. + * This is used to resolve a namespace prefix conflict in cases where spec does not want a + * namespace error in case of conflicts, but demands a resolution. */ +xmlNsPtr dom_get_ns_resolve_prefix_conflict(xmlNodePtr tree, const char *uri) +{ + ZEND_ASSERT(tree != NULL); + xmlDocPtr doc = tree->doc; + + if (UNEXPECTED(doc == NULL)) { + return NULL; + } + + /* Code adapted from libxml2 (2.10.4) */ + char prefix[50]; + int counter = 1; + snprintf(prefix, sizeof(prefix), "default"); + xmlNsPtr nsptr = xmlSearchNs(doc, tree, (const xmlChar *) prefix); + while (nsptr != NULL) { + if (counter > 1000) { + return NULL; + } + snprintf(prefix, sizeof(prefix), "default%d", counter++); + nsptr = xmlSearchNs(doc, tree, (const xmlChar *) prefix); + } + + /* Search yielded no conflict */ + return xmlNewNs(tree, (const xmlChar *) uri, (const xmlChar *) prefix); +} + /* http://www.w3.org/TR/2004/REC-DOM-Level-3-Core-20040407/core.html#ID-DocCrElNS @@ -1574,28 +1598,21 @@ xmlNsPtr dom_get_ns(xmlNodePtr nodep, char *uri, int *errorcode, char *prefix) { if (! ((prefix && !strcmp (prefix, "xml") && strcmp(uri, (char *)XML_XML_NAMESPACE)) || (prefix && !strcmp (prefix, "xmlns") && strcmp(uri, (char *)DOM_XMLNS_NAMESPACE)) || (prefix && !strcmp(uri, (char *)DOM_XMLNS_NAMESPACE) && strcmp (prefix, "xmlns")))) { - /* Reuse the old namespaces from doc->oldNs if possible, before creating a new one. - * This will prevent the oldNs list from growing with duplicates. */ - xmlDocPtr doc = nodep->doc; - if (doc && doc->oldNs != NULL) { - nsptr = doc->oldNs; - do { - if (xmlStrEqual(nsptr->prefix, (xmlChar *)prefix) && xmlStrEqual(nsptr->href, (xmlChar *)uri)) { - goto out; - } - nsptr = nsptr->next; - } while (nsptr); - } - /* Couldn't reuse one, create a new one. */ nsptr = xmlNewNs(nodep, (xmlChar *)uri, (xmlChar *)prefix); if (UNEXPECTED(nsptr == NULL)) { - goto err; + /* Either memory allocation failure, or it's because of a prefix conflict. + * We'll assume a conflict and try again. If it was a memory allocation failure we'll just fail again, whatever. + * 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?". + * This branch will also be taken unlikely anyway as in those cases it'll be for allocation failure. */ + nsptr = dom_get_ns_resolve_prefix_conflict(nodep, uri); + if (UNEXPECTED(nsptr == NULL)) { + goto err; + } } } else { goto err; } -out: *errorcode = 0; return nsptr; err: diff --git a/ext/dom/php_dom.h b/ext/dom/php_dom.h index b4a6a2d01e83b..4d415256788af 100644 --- a/ext/dom/php_dom.h +++ b/ext/dom/php_dom.h @@ -151,6 +151,7 @@ zend_string *dom_node_concatenated_name_helper(size_t name_len, const char *name zend_string *dom_node_get_node_name_attribute_or_element(const xmlNode *nodep); bool php_dom_is_node_connected(const xmlNode *node); bool php_dom_adopt_node(xmlNodePtr nodep, dom_object *dom_object_new_document, xmlDocPtr new_document); +xmlNsPtr dom_get_ns_resolve_prefix_conflict(xmlNodePtr tree, const char *uri); /* parentnode */ void dom_parent_node_prepend(dom_object *context, zval *nodes, uint32_t nodesc); diff --git a/ext/dom/tests/DOMDocument_importNode_attribute_prefix_conflict.phpt b/ext/dom/tests/DOMDocument_importNode_attribute_prefix_conflict.phpt new file mode 100644 index 0000000000000..d00fedbfb3d6c --- /dev/null +++ b/ext/dom/tests/DOMDocument_importNode_attribute_prefix_conflict.phpt @@ -0,0 +1,65 @@ +--TEST-- +DOMDocument::importNode() with attribute prefix name conflict +--EXTENSIONS-- +dom +--FILE-- +loadXML(''); +$dom2->loadXML(''); +$attribute = $dom1->documentElement->getAttributeNode('foo:bar'); +$imported = $dom2->importNode($attribute); +$dom2->documentElement->setAttributeNodeNS($imported); + +echo $dom1->saveXML(); +echo $dom2->saveXML(); + +echo "--- Non-default namespace test case with a default namespace in the destination ---\n"; + +$dom1 = new DOMDocument(); +$dom2 = new DOMDocument(); +$dom1->loadXML(''); +$dom2->loadXML(''); +$attribute = $dom1->documentElement->getAttributeNode('foo:bar'); +$imported = $dom2->importNode($attribute); +$dom2->documentElement->setAttributeNodeNS($imported); + +echo $dom1->saveXML(); +echo $dom2->saveXML(); + +echo "--- Default namespace test case ---\n"; + +// We don't expect the namespace to be imported because default namespaces on the same element don't apply to attributes +// but the attribute should be imported +$dom1 = new DOMDocument(); +$dom2 = new DOMDocument(); +$dom1->loadXML(''); +$dom2->loadXML(''); +$attribute = $dom1->documentElement->getAttributeNode('bar'); +$imported = $dom2->importNode($attribute); +$dom2->documentElement->setAttributeNodeNS($imported); + +echo $dom1->saveXML(); +echo $dom2->saveXML(); + +?> +--EXPECT-- +--- Non-default namespace test case without a default namespace in the destination --- + + + + +--- Non-default namespace test case with a default namespace in the destination --- + + + + +--- Default namespace test case --- + + + + diff --git a/ext/dom/tests/DOMElement_setAttributeNS_prefix_conflict.phpt b/ext/dom/tests/DOMElement_setAttributeNS_prefix_conflict.phpt new file mode 100644 index 0000000000000..fc491b59ea31d --- /dev/null +++ b/ext/dom/tests/DOMElement_setAttributeNS_prefix_conflict.phpt @@ -0,0 +1,35 @@ +--TEST-- +DOMElement::setAttributeNS() with prefix name conflict +--EXTENSIONS-- +dom +--FILE-- +loadXML(''); +$dom->documentElement->setAttributeNS('http://php.net/2', 'foo:bar', 'no1'); +echo $dom->saveXML(); +$dom->documentElement->setAttributeNS('http://php.net/2', 'bar', 'no2'); +echo $dom->saveXML(); + +echo "--- Default namespace test case ---\n"; + +$dom = new DOMDocument(); +$dom->loadXML(''); +$dom->documentElement->setAttributeNS('http://php.net/2', 'bar', 'no1'); +echo $dom->saveXML(); +$dom->documentElement->setAttributeNS('http://php.net/2', 'bar', 'no2'); +echo $dom->saveXML(); +?> +--EXPECT-- +--- Non-default namespace test case --- + + + + +--- Default namespace test case --- + + + + diff --git a/ext/dom/tests/createAttributeNS_prefix_conflicts/setAttributeNS_with_prefix.phpt b/ext/dom/tests/createAttributeNS_prefix_conflicts/setAttributeNS_with_prefix.phpt new file mode 100644 index 0000000000000..1ca679d576d2f --- /dev/null +++ b/ext/dom/tests/createAttributeNS_prefix_conflicts/setAttributeNS_with_prefix.phpt @@ -0,0 +1,36 @@ +--TEST-- +DOMDocument::createAttributeNS() with prefix name conflict - setAttributeNodeNS variation, with prefix +--EXTENSIONS-- +dom +--FILE-- +appendChild($doc->createElement('container')); + +var_dump($doc->documentElement->setAttributeNodeNS($doc->createAttributeNS('http://php.net/ns1', 'foo:hello'))?->namespaceURI); +echo $doc->saveXML(), "\n"; +var_dump($doc->documentElement->setAttributeNodeNS($doc->createAttributeNS('http://php.net/ns2', 'foo:hello'))?->namespaceURI); +echo $doc->saveXML(), "\n"; +var_dump($doc->documentElement->setAttributeNodeNS($doc->createAttributeNS('http://php.net/ns3', 'foo:hello'))?->namespaceURI); +echo $doc->saveXML(), "\n"; +var_dump($doc->documentElement->setAttributeNodeNS($doc->createAttributeNS('http://php.net/ns4', 'foo:hello'))?->namespaceURI); +echo $doc->saveXML(), "\n"; + +?> +--EXPECT-- +NULL + + + +NULL + + + +NULL + + + +NULL + + diff --git a/ext/dom/tests/createAttributeNS_prefix_conflicts/setAttributeNS_without_prefix.phpt b/ext/dom/tests/createAttributeNS_prefix_conflicts/setAttributeNS_without_prefix.phpt new file mode 100644 index 0000000000000..6ab6c2292a787 --- /dev/null +++ b/ext/dom/tests/createAttributeNS_prefix_conflicts/setAttributeNS_without_prefix.phpt @@ -0,0 +1,36 @@ +--TEST-- +DOMDocument::createAttributeNS() with prefix name conflict - setAttributeNodeNS variation, without prefix +--EXTENSIONS-- +dom +--FILE-- +appendChild($doc->createElement('container')); + +var_dump($doc->documentElement->setAttributeNodeNS($doc->createAttributeNS('http://php.net/ns1', 'hello'))?->namespaceURI); +echo $doc->saveXML(), "\n"; +var_dump($doc->documentElement->setAttributeNodeNS($doc->createAttributeNS('http://php.net/ns2', 'hello'))?->namespaceURI); +echo $doc->saveXML(), "\n"; +var_dump($doc->documentElement->setAttributeNodeNS($doc->createAttributeNS('http://php.net/ns3', 'hello'))?->namespaceURI); +echo $doc->saveXML(), "\n"; +var_dump($doc->documentElement->setAttributeNodeNS($doc->createAttributeNS('http://php.net/ns4', 'hello'))?->namespaceURI); +echo $doc->saveXML(), "\n"; + +?> +--EXPECT-- +NULL + + + +NULL + + + +NULL + + + +NULL + + diff --git a/ext/dom/tests/createAttributeNS_prefix_conflicts/setAttribute_mixed_prefix.phpt b/ext/dom/tests/createAttributeNS_prefix_conflicts/setAttribute_mixed_prefix.phpt new file mode 100644 index 0000000000000..a53d7304dc87e --- /dev/null +++ b/ext/dom/tests/createAttributeNS_prefix_conflicts/setAttribute_mixed_prefix.phpt @@ -0,0 +1,20 @@ +--TEST-- +DOMDocument::createAttributeNS() with prefix name conflict - setAttributeNode variation (DOM Level 3), mixed +--EXTENSIONS-- +dom +--FILE-- +appendChild($doc->createElement('container')); + +var_dump($doc->documentElement->setAttributeNode($doc->createAttributeNS('http://php.net/ns1', 'foo:hello'))?->namespaceURI); +var_dump($doc->documentElement->setAttributeNode($doc->createAttributeNS('http://php.net/ns1', 'hello'))?->namespaceURI); +echo $doc->saveXML(), "\n"; + +?> +--EXPECT-- +NULL +string(18) "http://php.net/ns1" + + diff --git a/ext/dom/tests/createAttributeNS_prefix_conflicts/setAttribute_with_prefix.phpt b/ext/dom/tests/createAttributeNS_prefix_conflicts/setAttribute_with_prefix.phpt new file mode 100644 index 0000000000000..161eadd353ba6 --- /dev/null +++ b/ext/dom/tests/createAttributeNS_prefix_conflicts/setAttribute_with_prefix.phpt @@ -0,0 +1,36 @@ +--TEST-- +DOMDocument::createAttributeNS() with prefix name conflict - setAttributeNode variation (DOM Level 3), with prefix +--EXTENSIONS-- +dom +--FILE-- +appendChild($doc->createElement('container')); + +var_dump($doc->documentElement->setAttributeNode($doc->createAttributeNS('http://php.net/ns1', 'foo:hello'))?->namespaceURI); +echo $doc->saveXML(), "\n"; +var_dump($doc->documentElement->setAttributeNode($doc->createAttributeNS('http://php.net/ns2', 'foo:hello'))?->namespaceURI); +echo $doc->saveXML(), "\n"; +var_dump($doc->documentElement->setAttributeNode($doc->createAttributeNS('http://php.net/ns3', 'foo:hello'))?->namespaceURI); +echo $doc->saveXML(), "\n"; +var_dump($doc->documentElement->setAttributeNode($doc->createAttributeNS('http://php.net/ns4', 'foo:hello'))?->namespaceURI); +echo $doc->saveXML(), "\n"; + +?> +--EXPECT-- +NULL + + + +string(18) "http://php.net/ns1" + + + +string(18) "http://php.net/ns2" + + + +string(18) "http://php.net/ns3" + + diff --git a/ext/dom/tests/createAttributeNS_prefix_conflicts/setAttribute_without_prefix.phpt b/ext/dom/tests/createAttributeNS_prefix_conflicts/setAttribute_without_prefix.phpt new file mode 100644 index 0000000000000..34717ff6ab5be --- /dev/null +++ b/ext/dom/tests/createAttributeNS_prefix_conflicts/setAttribute_without_prefix.phpt @@ -0,0 +1,36 @@ +--TEST-- +DOMDocument::createAttributeNS() with prefix name conflict - setAttributeNode variation (DOM Level 3), without prefix +--EXTENSIONS-- +dom +--FILE-- +appendChild($doc->createElement('container')); + +var_dump($doc->documentElement->setAttributeNode($doc->createAttributeNS('http://php.net/ns1', 'hello'))?->namespaceURI); +echo $doc->saveXML(), "\n"; +var_dump($doc->documentElement->setAttributeNode($doc->createAttributeNS('http://php.net/ns2', 'hello'))?->namespaceURI); +echo $doc->saveXML(), "\n"; +var_dump($doc->documentElement->setAttributeNode($doc->createAttributeNS('http://php.net/ns3', 'hello'))?->namespaceURI); +echo $doc->saveXML(), "\n"; +var_dump($doc->documentElement->setAttributeNode($doc->createAttributeNS('http://php.net/ns4', 'hello'))?->namespaceURI); +echo $doc->saveXML(), "\n"; + +?> +--EXPECT-- +NULL + + + +string(18) "http://php.net/ns1" + + + +string(18) "http://php.net/ns2" + + + +string(18) "http://php.net/ns3" + + diff --git a/ext/dom/tests/delayed_freeing/namespace_definition_crash_in_attribute.phpt b/ext/dom/tests/delayed_freeing/namespace_definition_crash_in_attribute.phpt index 64f2fbfa007a9..b43f7c34bae1d 100644 --- a/ext/dom/tests/delayed_freeing/namespace_definition_crash_in_attribute.phpt +++ b/ext/dom/tests/delayed_freeing/namespace_definition_crash_in_attribute.phpt @@ -39,9 +39,9 @@ echo $doc->saveXML($attr3->parentNode), "\n"; ?> --EXPECT-- - + - + string(15) "hello content 2" string(0) "" string(8) "some:ns2" @@ -49,5 +49,5 @@ NULL string(0) "" string(7) "some:ns" string(7) "some:ns" - hello="" + default1:hello=""