From 54a3ade9d76ad6cf935e5192fe523e6e720e3030 Mon Sep 17 00:00:00 2001 From: sricursion Date: Wed, 7 Oct 2026 12:24:45 +0530 Subject: [PATCH] gh-158978: Fix cloning of DOM Level 1 nodes in xml.dom.minidom cloneNode() and importNode() created the clones with createElementNS(), setAttributeNS() and createAttributeNS(), which now reject a prefixed name in no namespace. Create the Element and Attr nodes directly instead. --- Lib/test/test_minidom.py | 87 ++++++++++++++++++++++++++++++++++++++++ Lib/xml/dom/minidom.py | 17 +++++--- 2 files changed, 98 insertions(+), 6 deletions(-) diff --git a/Lib/test/test_minidom.py b/Lib/test/test_minidom.py index 446bbe096bd19d..c7bd81e590fafb 100644 --- a/Lib/test/test_minidom.py +++ b/Lib/test/test_minidom.py @@ -6,6 +6,7 @@ from test import support import unittest +import xml.dom.expatbuilder import xml.dom.minidom from xml.dom.minidom import parse, Attr, Node, Document, Element, parseString @@ -1277,6 +1278,65 @@ def testCloneElementDeep(self): self.assertTrue(clone.toxml(), '') dom.unlink() + def create_dom1_element(self): + # DOM Level 1 nodes have no namespace, but their names can contain + # a colon. + doc = Document() + root = doc.appendChild(doc.createElement("root")) + root.setAttribute("xmlns", "http://xml.python.org/default") + root.setAttribute("xmlns:xsi", "http://xml.python.org/xsi") + root.setAttribute("xsi:type", "t") + root.appendChild(doc.createElement("svg:rect")) + return doc, root + + def check_dom1_names(self, node, nodeName, prefix, localName): + self.assertEqual(node.nodeName, nodeName) + self.assertEqual(node.prefix, prefix) + self.assertEqual(node.localName, localName) + self.assertIsNone(node.namespaceURI) + + def check_dom1_element(self, elem, doc): + self.assertIs(elem.ownerDocument, doc) + self.check_dom1_names(elem, "root", None, "root") + self.assertEqual(len(elem.attributes), 3) + for name, prefix, localName, value in [ + ("xmlns", None, "xmlns", "http://xml.python.org/default"), + ("xmlns:xsi", "xmlns", "xsi", "http://xml.python.org/xsi"), + ("xsi:type", "xsi", "type", "t"), + ]: + with self.subTest(name=name): + attr = elem.getAttributeNode(name) + self.check_dom1_names(attr, name, prefix, localName) + self.assertEqual(attr.value, value) + self.assertIs(attr.ownerElement, elem) + self.assertIs(attr.ownerDocument, doc) + self.assertEqual(len(elem.childNodes), 1) + self.check_dom1_names(elem.firstChild, "svg:rect", "svg", "rect") + self.assertIs(elem.firstChild.ownerDocument, doc) + + def testCloneElementDOM1Names(self): + doc, root = self.create_dom1_element() + clone = root.cloneNode(True) + self.assertIsNot(clone, root) + self.assertIsNone(clone.parentNode) + self.check_dom1_element(clone, doc) + doc.unlink() + + def testCloneElementNoNamespaces(self): + doc = xml.dom.expatbuilder.parseString( + "", + namespaces=False) + clone = doc.documentElement.cloneNode(True) + self.check_dom1_names(clone, "a:b", "a", "b") + self.assertEqual(clone.getAttribute("xmlns:a"), + "http://xml.python.org/a") + self.check_dom1_names(clone.getAttributeNode("xmlns:a"), + "xmlns:a", "xmlns", "a") + self.assertEqual(clone.getAttribute("a:x"), "1") + self.check_dom1_names(clone.getAttributeNode("a:x"), "a:x", "a", "x") + self.check_dom1_names(clone.firstChild, "a:c", "a", "c") + doc.unlink() + def testCloneDocumentShallow(self): doc = parseString("\n" "" @@ -1395,6 +1455,15 @@ def testImportDocumentTypeDeep(self): self.assertRaises(xml.dom.NotSupportedErr, target.importNode, src.doctype, 1) + def testImportElementDOM1Names(self): + doc1, root = self.create_dom1_element() + doc2 = Document() + imported = doc2.importNode(root, True) + self.assertIsNone(imported.parentNode) + self.check_dom1_element(imported, doc2) + doc1.unlink() + doc2.unlink() + # Testing attribute clones uses a helper, and should always be deep, # even if the argument to cloneNode is false. def check_clone_attribute(self, deep, testName): @@ -1417,6 +1486,24 @@ def testCloneAttributeShallow(self): def testCloneAttributeDeep(self): self.check_clone_attribute(1, "testCloneAttributeDeep") + def testCloneAttributeDOM1Names(self): + doc1 = Document() + attr = doc1.createAttribute("a:b") + attr.value = "v" + doc2 = Document() + for method, clone, owner in [ + ("cloneNode", attr.cloneNode(True), doc1), + ("importNode", doc2.importNode(attr, True), doc2), + ]: + with self.subTest(method=method): + self.check_dom1_names(clone, "a:b", "a", "b") + self.assertEqual(clone.value, "v") + self.assertTrue(clone.specified) + self.assertIsNone(clone.ownerElement) + self.assertIs(clone.ownerDocument, owner) + doc1.unlink() + doc2.unlink() + def check_clone_pi(self, deep, testName): doc = parseString("") pi = doc.firstChild diff --git a/Lib/xml/dom/minidom.py b/Lib/xml/dom/minidom.py index 7cb652a323dcc2..0eedc678d7aac2 100644 --- a/Lib/xml/dom/minidom.py +++ b/Lib/xml/dom/minidom.py @@ -2169,11 +2169,15 @@ def _clone_node(node, deep, newOwnerDocument): else: operation = xml.dom.UserDataHandler.NODE_IMPORTED if node.nodeType == Node.ELEMENT_NODE: - clone = newOwnerDocument.createElementNS(node.namespaceURI, - node.nodeName) + prefix, localName = _nssplit(node.nodeName) + clone = Element(node.nodeName, node.namespaceURI, prefix) + clone.ownerDocument = newOwnerDocument for attr in node.attributes.values(): - clone.setAttributeNS(attr.namespaceURI, attr.nodeName, attr.value) - a = clone.getAttributeNodeNS(attr.namespaceURI, attr.localName) + prefix, localName = _nssplit(attr.nodeName) + a = Attr(attr.nodeName, attr.namespaceURI, localName, prefix) + a.value = attr.value + a.ownerDocument = newOwnerDocument + clone.setAttributeNode(a) a.specified = attr.specified if deep: @@ -2200,8 +2204,9 @@ def _clone_node(node, deep, newOwnerDocument): elif node.nodeType == Node.ENTITY_REFERENCE_NODE: clone = newOwnerDocument.createEntityReference(node.nodeName) elif node.nodeType == Node.ATTRIBUTE_NODE: - clone = newOwnerDocument.createAttributeNS(node.namespaceURI, - node.nodeName) + prefix, localName = _nssplit(node.nodeName) + clone = Attr(node.nodeName, node.namespaceURI, localName, prefix) + clone.ownerDocument = newOwnerDocument clone.specified = True clone.value = node.value elif node.nodeType == Node.DOCUMENT_TYPE_NODE: