diff --git a/changelogs/fragments/12148-xml-preserve-doctype.yml b/changelogs/fragments/12148-xml-preserve-doctype.yml
new file mode 100644
index 0000000000..4381f1e91e
--- /dev/null
+++ b/changelogs/fragments/12148-xml-preserve-doctype.yml
@@ -0,0 +1,2 @@
+bugfixes:
+ - "xml - preserve DOCTYPE declaration when writing modified XML files (https://github.com/ansible-collections/community.general/issues/2762, https://github.com/ansible-collections/community.general/pull/12148)."
diff --git a/plugins/modules/xml.py b/plugins/modules/xml.py
index 545246a3cf..66169f753b 100644
--- a/plugins/modules/xml.py
+++ b/plugins/modules/xml.py
@@ -813,9 +813,15 @@ def children_to_nodes(module=None, children=None, type="yaml"):
def make_pretty(module, tree):
- xml_string = etree.tostring(
- tree, xml_declaration=True, encoding="UTF-8", pretty_print=module.params["pretty_print"]
+ buf = BytesIO()
+ tree.write(
+ buf,
+ xml_declaration=True,
+ encoding="UTF-8",
+ pretty_print=module.params["pretty_print"],
+ doctype=tree.docinfo.doctype or None,
)
+ xml_string = buf.getvalue()
result = dict(
changed=False,
@@ -830,7 +836,11 @@ def make_pretty(module, tree):
if module.params["backup"]:
result["backup_file"] = module.backup_local(module.params["path"])
tree.write(
- xml_file, xml_declaration=True, encoding="UTF-8", pretty_print=module.params["pretty_print"]
+ xml_file,
+ xml_declaration=True,
+ encoding="UTF-8",
+ pretty_print=module.params["pretty_print"],
+ doctype=tree.docinfo.doctype or None,
)
elif module.params["xmlstring"]:
@@ -859,10 +869,23 @@ def finish(module, tree, xpath, namespaces, changed=False, msg="", hitcount=0, m
if result["changed"]:
if module._diff:
- result["diff"] = dict(
- before=etree.tostring(orig_doc, xml_declaration=True, encoding="UTF-8", pretty_print=True),
- after=etree.tostring(tree, xml_declaration=True, encoding="UTF-8", pretty_print=True),
+ before_buf = BytesIO()
+ orig_doc.write(
+ before_buf,
+ xml_declaration=True,
+ encoding="UTF-8",
+ pretty_print=True,
+ doctype=orig_doc.docinfo.doctype or None,
)
+ after_buf = BytesIO()
+ tree.write(
+ after_buf,
+ xml_declaration=True,
+ encoding="UTF-8",
+ pretty_print=True,
+ doctype=tree.docinfo.doctype or None,
+ )
+ result["diff"] = dict(before=before_buf.getvalue(), after=after_buf.getvalue())
if module.params["path"] and not module.check_mode:
if module.params["backup"]:
@@ -872,12 +895,19 @@ def finish(module, tree, xpath, namespaces, changed=False, msg="", hitcount=0, m
xml_declaration=True,
encoding="UTF-8",
pretty_print=module.params["pretty_print"],
+ doctype=tree.docinfo.doctype or None,
)
if module.params["xmlstring"]:
- result["xmlstring"] = etree.tostring(
- tree, xml_declaration=True, encoding="UTF-8", pretty_print=module.params["pretty_print"]
+ xmlstring_buf = BytesIO()
+ tree.write(
+ xmlstring_buf,
+ xml_declaration=True,
+ encoding="UTF-8",
+ pretty_print=module.params["pretty_print"],
+ doctype=tree.docinfo.doctype or None,
)
+ result["xmlstring"] = xmlstring_buf.getvalue()
module.exit_json(**result)
diff --git a/tests/integration/targets/xml/fixtures/ansible-xml-doctype.xml b/tests/integration/targets/xml/fixtures/ansible-xml-doctype.xml
new file mode 100644
index 0000000000..3b755c1f54
--- /dev/null
+++ b/tests/integration/targets/xml/fixtures/ansible-xml-doctype.xml
@@ -0,0 +1,3 @@
+
+
+
diff --git a/tests/integration/targets/xml/fixtures/ansible-xml-doctype.xml.license b/tests/integration/targets/xml/fixtures/ansible-xml-doctype.xml.license
new file mode 100644
index 0000000000..edff8c7685
--- /dev/null
+++ b/tests/integration/targets/xml/fixtures/ansible-xml-doctype.xml.license
@@ -0,0 +1,3 @@
+GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+SPDX-License-Identifier: GPL-3.0-or-later
+SPDX-FileCopyrightText: Ansible Project
diff --git a/tests/integration/targets/xml/results/test-preserve-doctype.xml b/tests/integration/targets/xml/results/test-preserve-doctype.xml
new file mode 100644
index 0000000000..72aced83d2
--- /dev/null
+++ b/tests/integration/targets/xml/results/test-preserve-doctype.xml
@@ -0,0 +1,3 @@
+
+
+
diff --git a/tests/integration/targets/xml/results/test-preserve-doctype.xml.license b/tests/integration/targets/xml/results/test-preserve-doctype.xml.license
new file mode 100644
index 0000000000..edff8c7685
--- /dev/null
+++ b/tests/integration/targets/xml/results/test-preserve-doctype.xml.license
@@ -0,0 +1,3 @@
+GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+SPDX-License-Identifier: GPL-3.0-or-later
+SPDX-FileCopyrightText: Ansible Project
diff --git a/tests/integration/targets/xml/tasks/main.yml b/tests/integration/targets/xml/tasks/main.yml
index e69a28e578..257f29a100 100644
--- a/tests/integration/targets/xml/tasks/main.yml
+++ b/tests/integration/targets/xml/tasks/main.yml
@@ -79,6 +79,7 @@
- include_tasks: test-print-match.yml
- include_tasks: test-xmlstring.yml
- include_tasks: test-children-elements-xml.yml
+ - include_tasks: test-preserve-doctype.yml
# Unicode tests
- include_tasks: test-add-children-elements-unicode.yml
diff --git a/tests/integration/targets/xml/tasks/test-preserve-doctype.yml b/tests/integration/targets/xml/tasks/test-preserve-doctype.yml
new file mode 100644
index 0000000000..5c5b61400b
--- /dev/null
+++ b/tests/integration/targets/xml/tasks/test-preserve-doctype.yml
@@ -0,0 +1,46 @@
+# Copyright (c) Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+- name: Setup test fixture
+ copy:
+ src: fixtures/ansible-xml-doctype.xml
+ dest: /tmp/ansible-xml-doctype.xml
+
+- name: Set '/foo/@value' to '1' (file has DOCTYPE)
+ xml:
+ path: /tmp/ansible-xml-doctype.xml
+ xpath: /foo
+ attribute: value
+ value: "1"
+ register: set_attribute_doctype
+
+- name: Add trailing newline
+ shell: echo "" >> /tmp/ansible-xml-doctype.xml
+
+- name: Compare to expected result
+ copy:
+ src: results/test-preserve-doctype.xml
+ dest: /tmp/ansible-xml-doctype.xml
+ check_mode: true
+ diff: true
+ register: comparison
+
+- name: Test expected result (DOCTYPE is preserved)
+ assert:
+ that:
+ - set_attribute_doctype is changed
+ - comparison is not changed
+
+- name: Set '/foo/@value' to '1' again (idempotency)
+ xml:
+ path: /tmp/ansible-xml-doctype.xml
+ xpath: /foo
+ attribute: value
+ value: "1"
+ register: set_attribute_doctype_idempotent
+
+- name: Test idempotency
+ assert:
+ that:
+ - set_attribute_doctype_idempotent is not changed