1    	/*
2    	 * Copyright 2004-2026 the Pacemaker project contributors
3    	 *
4    	 * The version control history for this file may have further details.
5    	 *
6    	 * This source code is licensed under the GNU Lesser General Public License
7    	 * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
8    	 */
9    	
10   	#include <crm_internal.h>
11   	
12   	#include <stdarg.h>
13   	#include <stdbool.h>
14   	#include <stdint.h>                     // uint32_t
15   	#include <stdio.h>
16   	#include <stdlib.h>
17   	#include <string.h>
18   	#include <sys/stat.h>                   // stat(), S_ISREG, etc.
19   	#include <sys/types.h>
20   	
21   	#include <glib.h>                       // gboolean, GString
22   	#include <libxml/tree.h>                // xmlNode, etc.
23   	#include <libxml/xmlstring.h>           // xmlChar, xmlGetUTF8Char()
24   	
25   	#include <crm/crm.h>
26   	#include <crm/common/xml.h>
27   	#include "crmcommon_private.h"
28   	
29   	//! libxml2 supports only XML version 1.0, at least as of libxml2-2.12.5
30   	#define XML_VERSION ((const xmlChar *) "1.0")
31   	
32   	/*!
33   	 * \internal
34   	 * \brief Get a string representation of an XML element type for logging
35   	 *
36   	 * \param[in] type  XML element type
37   	 *
38   	 * \return String representation of \p type
39   	 */
40   	const char *
41   	pcmk__xml_element_type_text(xmlElementType type)
42   	{
43   	    static const char *const element_type_names[] = {
44   	        [XML_ELEMENT_NODE]       = "element",
45   	        [XML_ATTRIBUTE_NODE]     = "attribute",
46   	        [XML_TEXT_NODE]          = "text",
47   	        [XML_CDATA_SECTION_NODE] = "CDATA section",
48   	        [XML_ENTITY_REF_NODE]    = "entity reference",
49   	        [XML_ENTITY_NODE]        = "entity",
50   	        [XML_PI_NODE]            = "PI",
51   	        [XML_COMMENT_NODE]       = "comment",
52   	        [XML_DOCUMENT_NODE]      = "document",
53   	        [XML_DOCUMENT_TYPE_NODE] = "document type",
54   	        [XML_DOCUMENT_FRAG_NODE] = "document fragment",
55   	        [XML_NOTATION_NODE]      = "notation",
56   	        [XML_HTML_DOCUMENT_NODE] = "HTML document",
57   	        [XML_DTD_NODE]           = "DTD",
58   	        [XML_ELEMENT_DECL]       = "element declaration",
59   	        [XML_ATTRIBUTE_DECL]     = "attribute declaration",
60   	        [XML_ENTITY_DECL]        = "entity declaration",
61   	        [XML_NAMESPACE_DECL]     = "namespace declaration",
62   	        [XML_XINCLUDE_START]     = "XInclude start",
63   	        [XML_XINCLUDE_END]       = "XInclude end",
64   	    };
65   	
66   	    // Assumes the numeric values of the indices are in ascending order
67   	    if ((type < XML_ELEMENT_NODE) || (type > XML_XINCLUDE_END)) {
68   	        return "unrecognized type";
69   	    }
70   	    return element_type_names[type];
71   	}
72   	
73   	/*!
74   	 * \internal
75   	 * \brief Apply a function to each XML node in a tree (pre-order, depth-first)
76   	 *
77   	 * \param[in,out] xml        XML tree to traverse
78   	 * \param[in,out] fn         Function to call for each node (returns \c true to
79   	 *                           continue traversing the tree or \c false to stop)
80   	 * \param[in,out] user_data  Argument to \p fn
81   	 *
82   	 * \return \c false if any \p fn call returned \c false, or \c true otherwise
83   	 *
84   	 * \note This function is recursive.
85   	 */
86   	bool
87   	pcmk__xml_tree_foreach(xmlNode *xml, bool (*fn)(xmlNode *, void *),
88   	                       void *user_data)
89   	{
90   	    if (xml == NULL) {
91   	        return true;
92   	    }
93   	
94   	    if (!fn(xml, user_data)) {
95   	        return false;
96   	    }
97   	
98   	    for (xml = pcmk__xml_first_child(xml); xml != NULL;
99   	         xml = pcmk__xml_next(xml)) {
100  	
101  	        if (!pcmk__xml_tree_foreach(xml, fn, user_data)) {
102  	            return false;
103  	        }
104  	    }
105  	    return true;
106  	}
107  	
108  	void
109  	pcmk__xml_set_parent_flags(xmlNode *xml, uint64_t flags)
110  	{
111  	    for (; xml != NULL; xml = xml->parent) {
112  	        xml_node_private_t *nodepriv = xml->_private;
113  	
114  	        if (nodepriv != NULL) {
115  	            pcmk__set_xml_flags(nodepriv, flags);
116  	        }
117  	    }
118  	}
119  	
120  	/*!
121  	 * \internal
122  	 * \brief Set flags for an XML document
123  	 *
124  	 * \param[in,out] doc    XML document
125  	 * \param[in]     flags  Group of <tt>enum pcmk__xml_flags</tt>
126  	 */
127  	void
128  	pcmk__xml_doc_set_flags(xmlDoc *doc, uint32_t flags)
129  	{
130  	    if (doc != NULL) {
131  	        xml_doc_private_t *docpriv = doc->_private;
132  	
133  	        pcmk__set_xml_flags(docpriv, flags);
134  	    }
135  	}
136  	
137  	/*!
138  	 * \internal
139  	 * \brief Check whether the given flags are set for an XML document
140  	 *
141  	 * \param[in] doc    XML document to check
142  	 * \param[in] flags  Group of <tt>enum pcmk__xml_flags</tt>
143  	 *
144  	 * \return \c true if all of \p flags are set for \p doc, or \c false otherwise
145  	 */
146  	bool
147  	pcmk__xml_doc_all_flags_set(const xmlDoc *doc, uint32_t flags)
148  	{
149  	    if (doc != NULL) {
150  	        xml_doc_private_t *docpriv = doc->_private;
151  	
152  	        return (docpriv != NULL) && pcmk__all_flags_set(docpriv->flags, flags);
153  	    }
154  	    return false;
155  	}
156  	
157  	// Mark document, element, and all element's parents as changed
158  	void
159  	pcmk__mark_xml_node_dirty(xmlNode *xml)
160  	{
161  	    if (xml == NULL) {
162  	        return;
163  	    }
164  	    pcmk__xml_doc_set_flags(xml->doc, pcmk__xf_dirty);
165  	    pcmk__xml_set_parent_flags(xml, pcmk__xf_dirty);
166  	}
167  	
168  	/*!
169  	 * \internal
170  	 * \brief Clear flags on an XML node
171  	 *
172  	 * \param[in,out] xml        XML node whose flags to reset
173  	 * \param[in,out] user_data  Ignored
174  	 *
175  	 * \return \c true (to continue traversing the tree)
176  	 *
177  	 * \note This is compatible with \c pcmk__xml_tree_foreach().
178  	 */
179  	bool
180  	pcmk__xml_reset_node_flags(xmlNode *xml, void *user_data)
181  	{
182  	    xml_node_private_t *nodepriv = xml->_private;
183  	
184  	    if (nodepriv != NULL) {
185  	        nodepriv->flags = pcmk__xf_none;
186  	    }
187  	    return true;
188  	}
189  	
190  	/*!
191  	 * \internal
192  	 * \brief Set the \c pcmk__xf_dirty and \c pcmk__xf_created flags on an XML node
193  	 *
194  	 * \param[in,out] xml        Node whose flags to set
195  	 * \param[in]     user_data  Ignored
196  	 *
197  	 * \return \c true (to continue traversing the tree)
198  	 *
199  	 * \note This is compatible with \c pcmk__xml_tree_foreach().
200  	 */
201  	static bool
202  	mark_xml_dirty_created(xmlNode *xml, void *user_data)
203  	{
204  	    xml_node_private_t *nodepriv = xml->_private;
205  	
206  	    if (nodepriv != NULL) {
207  	        pcmk__set_xml_flags(nodepriv, pcmk__xf_dirty|pcmk__xf_created);
208  	    }
209  	    return true;
210  	}
211  	
212  	/*!
213  	 * \internal
214  	 * \brief Mark an XML tree as dirty and created, and mark its parents dirty
215  	 *
216  	 * Also mark the document dirty.
217  	 *
218  	 * \param[in,out] xml  Tree to mark as dirty and created
219  	 */
220  	static void
221  	mark_xml_tree_dirty_created(xmlNode *xml)
222  	{
223  	    pcmk__assert(xml != NULL);
224  	
225  	    if (!pcmk__xml_doc_all_flags_set(xml->doc, pcmk__xf_tracking)) {
226  	        // Tracking is disabled for entire document
227  	        return;
228  	    }
229  	
230  	    // Mark all parents and document dirty
231  	    pcmk__mark_xml_node_dirty(xml);
232  	
233  	    pcmk__xml_tree_foreach(xml, mark_xml_dirty_created, NULL);
234  	}
235  	
236  	// Free an XML object previously marked as deleted
237  	static void
238  	free_deleted_object(void *data)
239  	{
240  	    if(data) {
241  	        pcmk__deleted_xml_t *deleted_obj = data;
242  	
243  	        g_free(deleted_obj->path);
244  	        free(deleted_obj);
245  	    }
246  	}
247  	
248  	// Free and NULL user, ACLs, and deleted objects in an XML node's private data
249  	static void
250  	reset_xml_private_data(xml_doc_private_t *docpriv)
251  	{
(1) Event path: Condition "docpriv != NULL", taking true branch.
252  	    if (docpriv != NULL) {
(2) Event path: Condition "!(docpriv->check == 2171757396UL)", taking false branch.
253  	        pcmk__assert(docpriv->check == PCMK__XML_DOC_PRIVATE_MAGIC);
254  	
(3) Event path: Condition "_p", taking true branch.
255  	        g_clear_pointer(&docpriv->acl_user, free);
CID (unavailable; MK=dd9fe254ce53b94eaf7be072f227e066) (#2 of 2): Inconsistent C union access (INCONSISTENT_UNION_ACCESS):
(4) Event assign_union_field: The union field "in" of "_pp" is written.
(5) Event inconsistent_union_field_access: In "_pp.out", the union field used: "out" is inconsistent with the field most recently stored: "in".
256  	        g_clear_pointer(&docpriv->acls, pcmk__free_acls);
257  	
258  	        g_list_free_full(docpriv->deleted_objs, free_deleted_object);
259  	        docpriv->deleted_objs = NULL;
260  	    }
261  	}
262  	
263  	/*!
264  	 * \internal
265  	 * \brief Allocate and initialize private data for an XML node
266  	 *
267  	 * \param[in,out] node       XML node whose private data to initialize
268  	 * \param[in]     user_data  Ignored
269  	 *
270  	 * \return \c true (to continue traversing the tree)
271  	 *
272  	 * \note This is compatible with \c pcmk__xml_tree_foreach().
273  	 */
274  	static bool
275  	new_private_data(xmlNode *node, void *user_data)
276  	{
277  	    bool tracking = false;
278  	
279  	    CRM_CHECK(node != NULL, return true);
280  	
281  	    if (node->_private != NULL) {
282  	        return true;
283  	    }
284  	
285  	    tracking = pcmk__xml_doc_all_flags_set(node->doc, pcmk__xf_tracking);
286  	
287  	    switch (node->type) {
288  	        case XML_DOCUMENT_NODE:
289  	            {
290  	                xml_doc_private_t *docpriv =
291  	                    pcmk__assert_alloc(1, sizeof(xml_doc_private_t));
292  	
293  	                docpriv->check = PCMK__XML_DOC_PRIVATE_MAGIC;
294  	                node->_private = docpriv;
295  	            }
296  	            break;
297  	
298  	        case XML_ELEMENT_NODE:
299  	        case XML_ATTRIBUTE_NODE:
300  	        case XML_COMMENT_NODE:
301  	            {
302  	                xml_node_private_t *nodepriv =
303  	                    pcmk__assert_alloc(1, sizeof(xml_node_private_t));
304  	
305  	                nodepriv->check = PCMK__XML_NODE_PRIVATE_MAGIC;
306  	                node->_private = nodepriv;
307  	                if (tracking) {
308  	                    pcmk__set_xml_flags(nodepriv, pcmk__xf_dirty|pcmk__xf_created);
309  	                }
310  	
311  	                for (xmlAttr *iter = pcmk__xe_first_attr(node); iter != NULL;
312  	                     iter = iter->next) {
313  	
314  	                    new_private_data((xmlNode *) iter, user_data);
315  	                }
316  	            }
317  	            break;
318  	
319  	        case XML_TEXT_NODE:
320  	        case XML_DTD_NODE:
321  	        case XML_CDATA_SECTION_NODE:
322  	            return true;
323  	
324  	        default:
325  	            CRM_LOG_ASSERT(node->type == XML_ELEMENT_NODE);
326  	            return true;
327  	    }
328  	
329  	    if (tracking) {
330  	        pcmk__mark_xml_node_dirty(node);
331  	    }
332  	    return true;
333  	}
334  	
335  	/*!
336  	 * \internal
337  	 * \brief Free private data for an XML node
338  	 *
339  	 * \param[in,out] node       XML node whose private data to free
340  	 * \param[in]     user_data  Ignored
341  	 *
342  	 * \return \c true (to continue traversing the tree)
343  	 *
344  	 * \note This is compatible with \c pcmk__xml_tree_foreach().
345  	 */
346  	static bool
347  	free_private_data(xmlNode *node, void *user_data)
348  	{
349  	    CRM_CHECK(node != NULL, return true);
350  	
351  	    if (node->_private == NULL) {
352  	        return true;
353  	    }
354  	
355  	    if (node->type == XML_DOCUMENT_NODE) {
356  	        reset_xml_private_data((xml_doc_private_t *) node->_private);
357  	
358  	    } else {
359  	        xml_node_private_t *nodepriv = node->_private;
360  	
361  	        pcmk__assert(nodepriv->check == PCMK__XML_NODE_PRIVATE_MAGIC);
362  	
363  	        for (xmlAttr *iter = pcmk__xe_first_attr(node); iter != NULL;
364  	             iter = iter->next) {
365  	
366  	            free_private_data((xmlNode *) iter, user_data);
367  	        }
368  	    }
369  	
370  	    g_clear_pointer(&node->_private, free);
371  	    return true;
372  	}
373  	
374  	/*!
375  	 * \internal
376  	 * \brief Allocate and initialize private data recursively for an XML tree
377  	 *
378  	 * \param[in,out] node  XML node whose private data to initialize
379  	 */
380  	void
381  	pcmk__xml_new_private_data(xmlNode *xml)
382  	{
383  	    pcmk__xml_tree_foreach(xml, new_private_data, NULL);
384  	}
385  	
386  	/*!
387  	 * \internal
388  	 * \brief Free private data recursively for an XML tree
389  	 *
390  	 * \param[in,out] node  XML node whose private data to free
391  	 */
392  	void
393  	pcmk__xml_free_private_data(xmlNode *xml)
394  	{
395  	    pcmk__xml_tree_foreach(xml, free_private_data, NULL);
396  	}
397  	
398  	/*!
399  	 * \internal
400  	 * \brief Return ordinal position of an XML node among its siblings
401  	 *
402  	 * \param[in] xml            XML node to check
403  	 * \param[in] ignore_if_set  Don't count siblings with this flag set
404  	 *
405  	 * \return Ordinal position of \p xml (starting with 0)
406  	 */
407  	int
408  	pcmk__xml_position(const xmlNode *xml, enum pcmk__xml_flags ignore_if_set)
409  	{
410  	    int position = 0;
411  	
412  	    for (const xmlNode *cIter = xml; cIter->prev; cIter = cIter->prev) {
413  	        xml_node_private_t *nodepriv = ((xmlNode*)cIter->prev)->_private;
414  	
415  	        if (!pcmk__is_set(nodepriv->flags, ignore_if_set)) {
416  	            position++;
417  	        }
418  	    }
419  	
420  	    return position;
421  	}
422  	
423  	/*!
424  	 * \internal
425  	 * \brief Remove all attributes marked as deleted from an XML node
426  	 *
427  	 * \param[in,out] xml        XML node whose deleted attributes to remove
428  	 * \param[in,out] user_data  Ignored
429  	 *
430  	 * \return \c true (to continue traversing the tree)
431  	 *
432  	 * \note This is compatible with \c pcmk__xml_tree_foreach().
433  	 */
434  	static bool
435  	commit_attr_deletions(xmlNode *xml, void *user_data)
436  	{
437  	    pcmk__xml_reset_node_flags(xml, NULL);
438  	    pcmk__xe_remove_matching_attrs(xml, true, pcmk__marked_as_deleted, NULL);
439  	    return true;
440  	}
441  	
442  	/*!
443  	 * \internal
444  	 * \brief Finalize all pending changes to an XML document and reset private data
445  	 *
446  	 * Clear the ACL user and all flags, unpacked ACLs, and deleted node records for
447  	 * the document; clear all flags on each node in the tree; and delete any
448  	 * attributes that are marked for deletion.
449  	 *
450  	 * \param[in,out] doc  XML document
451  	 *
452  	 * \note When change tracking is enabled, "deleting" an attribute simply marks
453  	 *       it for deletion (using \c pcmk__xf_deleted) until changes are
454  	 *       committed. Freeing a node (using \c pcmk__xml_free()) adds a deleted
455  	 *       node record (\c pcmk__deleted_xml_t) to the node's document before
456  	 *       freeing it.
457  	 * \note This function clears all flags, not just flags that indicate changes.
458  	 *       In particular, note that it clears the \c pcmk__xf_tracking flag, thus
459  	 *       disabling tracking.
460  	 */
461  	void
462  	pcmk__xml_commit_changes(xmlDoc *doc)
463  	{
464  	    xml_doc_private_t *docpriv = NULL;
465  	
466  	    if (doc == NULL) {
467  	        return;
468  	    }
469  	
470  	    docpriv = doc->_private;
471  	    if (docpriv == NULL) {
472  	        return;
473  	    }
474  	
475  	    if (pcmk__is_set(docpriv->flags, pcmk__xf_dirty)) {
476  	        pcmk__xml_tree_foreach(xmlDocGetRootElement(doc), commit_attr_deletions,
477  	                               NULL);
478  	    }
479  	    reset_xml_private_data(docpriv);
480  	    docpriv->flags = pcmk__xf_none;
481  	}
482  	
483  	/*!
484  	 * \internal
485  	 * \brief Create a new XML document
486  	 *
487  	 * \return Newly allocated XML document (guaranteed not to be \c NULL)
488  	 *
489  	 * \note The caller is responsible for freeing the return value using
490  	 *       \c pcmk__xml_free_doc().
491  	 */
492  	xmlDoc *
493  	pcmk__xml_new_doc(void)
494  	{
495  	    xmlDoc *doc = xmlNewDoc(XML_VERSION);
496  	
497  	    pcmk__mem_assert(doc);
498  	    pcmk__xml_new_private_data((xmlNode *) doc);
499  	    return doc;
500  	}
501  	
502  	/*!
503  	 * \internal
504  	 * \brief Free a new XML document
505  	 *
506  	 * \param[in,out] doc  XML document to free
507  	 */
508  	void
509  	pcmk__xml_free_doc(xmlDoc *doc)
510  	{
511  	    if (doc != NULL) {
512  	        pcmk__xml_free_private_data((xmlNode *) doc);
513  	        xmlFreeDoc(doc);
514  	    }
515  	}
516  	
517  	/*!
518  	 * \internal
519  	 * \brief Check whether the first character of a string is an XML NameStartChar
520  	 *
521  	 * See https://www.w3.org/TR/xml/#NT-NameStartChar.
522  	 *
523  	 * This is almost identical to libxml2's \c xmlIsDocNameStartChar(), but they
524  	 * don't expose it as part of the public API.
525  	 *
526  	 * \param[in]  utf8  UTF-8 encoded string
527  	 * \param[out] len   If not \c NULL, where to store size in bytes of first
528  	 *                   character in \p utf8
529  	 *
530  	 * \return \c true if \p utf8 begins with a valid XML NameStartChar, or \c false
531  	 *         otherwise
532  	 */
533  	bool
534  	pcmk__xml_is_name_start_char(const char *utf8, int *len)
535  	{
536  	    int c = 0;
537  	    int local_len = 0;
538  	
539  	    if (len == NULL) {
540  	        len = &local_len;
541  	    }
542  	
543  	    /* xmlGetUTF8Char() abuses the len argument. At call time, it must be set to
544  	     * "the minimum number of bytes present in the sequence... to assure the
545  	     * next character is completely contained within the sequence." It's similar
546  	     * to the "n" in the strn*() functions. However, this doesn't make any sense
547  	     * for null-terminated strings, and there's no value that indicates "keep
548  	     * going until '\0'." So we set it to 4, the max number of bytes in a UTF-8
549  	     * character.
550  	     *
551  	     * At return, it's set to the actual number of bytes in the char, or 0 on
552  	     * error.
553  	     */
554  	    *len = 4;
555  	
556  	    // Note: xmlGetUTF8Char() assumes a 32-bit int
557  	    c = xmlGetUTF8Char((const xmlChar *) utf8, len);
558  	    if (c < 0) {
559  	        GString *buf = g_string_sized_new(32);
560  	
561  	        for (int i = 0; (i < 4) && (utf8[i] != '\0'); i++) {
562  	            g_string_append_printf(buf, " 0x%.2X", utf8[i]);
563  	        }
564  	        pcmk__info("Invalid UTF-8 character (bytes:%s)",
565  	                   (pcmk__str_empty(buf->str)? " <none>" : buf->str));
566  	        g_string_free(buf, TRUE);
567  	        return false;
568  	    }
569  	
570  	    return (c == '_')
571  	           || (c == ':')
572  	           || ((c >= 'a') && (c <= 'z'))
573  	           || ((c >= 'A') && (c <= 'Z'))
574  	           || ((c >= 0xC0) && (c <= 0xD6))
575  	           || ((c >= 0xD8) && (c <= 0xF6))
576  	           || ((c >= 0xF8) && (c <= 0x2FF))
577  	           || ((c >= 0x370) && (c <= 0x37D))
578  	           || ((c >= 0x37F) && (c <= 0x1FFF))
579  	           || ((c >= 0x200C) && (c <= 0x200D))
580  	           || ((c >= 0x2070) && (c <= 0x218F))
581  	           || ((c >= 0x2C00) && (c <= 0x2FEF))
582  	           || ((c >= 0x3001) && (c <= 0xD7FF))
583  	           || ((c >= 0xF900) && (c <= 0xFDCF))
584  	           || ((c >= 0xFDF0) && (c <= 0xFFFD))
585  	           || ((c >= 0x10000) && (c <= 0xEFFFF));
586  	}
587  	
588  	/*!
589  	 * \internal
590  	 * \brief Check whether the first character of a string is an XML NameChar
591  	 *
592  	 * See https://www.w3.org/TR/xml/#NT-NameChar.
593  	 *
594  	 * This is almost identical to libxml2's \c xmlIsDocNameChar(), but they don't
595  	 * expose it as part of the public API.
596  	 *
597  	 * \param[in]  utf8  UTF-8 encoded string
598  	 * \param[out] len   If not \c NULL, where to store size in bytes of first
599  	 *                   character in \p utf8
600  	 *
601  	 * \return \c true if \p utf8 begins with a valid XML NameChar, or \c false
602  	 *         otherwise
603  	 */
604  	bool
605  	pcmk__xml_is_name_char(const char *utf8, int *len)
606  	{
607  	    int c = 0;
608  	    int local_len = 0;
609  	
610  	    if (len == NULL) {
611  	        len = &local_len;
612  	    }
613  	
614  	    // See comment regarding len in pcmk__xml_is_name_start_char()
615  	    *len = 4;
616  	
617  	    // Note: xmlGetUTF8Char() assumes a 32-bit int
618  	    c = xmlGetUTF8Char((const xmlChar *) utf8, len);
619  	    if (c < 0) {
620  	        GString *buf = g_string_sized_new(32);
621  	
622  	        for (int i = 0; (i < 4) && (utf8[i] != '\0'); i++) {
623  	            g_string_append_printf(buf, " 0x%.2X", utf8[i]);
624  	        }
625  	        pcmk__info("Invalid UTF-8 character (bytes:%s)",
626  	                   (pcmk__str_empty(buf->str)? " <none>" : buf->str));
627  	        g_string_free(buf, TRUE);
628  	        return false;
629  	    }
630  	
631  	    return ((c >= 'a') && (c <= 'z'))
632  	           || ((c >= 'A') && (c <= 'Z'))
633  	           || ((c >= '0') && (c <= '9'))
634  	           || (c == '_')
635  	           || (c == ':')
636  	           || (c == '-')
637  	           || (c == '.')
638  	           || (c == 0xB7)
639  	           || ((c >= 0xC0) && (c <= 0xD6))
640  	           || ((c >= 0xD8) && (c <= 0xF6))
641  	           || ((c >= 0xF8) && (c <= 0x2FF))
642  	           || ((c >= 0x300) && (c <= 0x36F))
643  	           || ((c >= 0x370) && (c <= 0x37D))
644  	           || ((c >= 0x37F) && (c <= 0x1FFF))
645  	           || ((c >= 0x200C) && (c <= 0x200D))
646  	           || ((c >= 0x203F) && (c <= 0x2040))
647  	           || ((c >= 0x2070) && (c <= 0x218F))
648  	           || ((c >= 0x2C00) && (c <= 0x2FEF))
649  	           || ((c >= 0x3001) && (c <= 0xD7FF))
650  	           || ((c >= 0xF900) && (c <= 0xFDCF))
651  	           || ((c >= 0xFDF0) && (c <= 0xFFFD))
652  	           || ((c >= 0x10000) && (c <= 0xEFFFF));
653  	}
654  	
655  	/*!
656  	 * \internal
657  	 * \brief Sanitize a string so it is usable as an XML ID
658  	 *
659  	 * An ID must match the Name production as defined here:
660  	 * https://www.w3.org/TR/xml/#NT-Name.
661  	 *
662  	 * Convert an invalid start character to \c '_'. Convert an invalid character
663  	 * after the start character to \c '.'.
664  	 *
665  	 * \param[in,out] id  String to sanitize
666  	 */
667  	void
668  	pcmk__xml_sanitize_id(char *id)
669  	{
670  	    bool valid = true;
671  	    int len = 0;
672  	
673  	    // If id is empty or NULL, there's no way to make it a valid XML ID
674  	    pcmk__assert(!pcmk__str_empty(id));
675  	
676  	    /* @TODO Suppose there are two strings and each has an invalid ID character
677  	     * in the same position. The strings are otherwise identical. Both strings
678  	     * will be sanitized to the same valid ID, which is incorrect.
679  	     *
680  	     * The caller is responsible for ensuring the sanitized ID does not already
681  	     * exist in a given XML document before using it, if uniqueness is desired.
682  	     */
683  	    valid = pcmk__xml_is_name_start_char(id, &len);
684  	    CRM_CHECK(len > 0, return); // UTF-8 encoding error
685  	    if (!valid) {
686  	        *id = '_';
687  	        for (int i = 1; i < len; i++) {
688  	            id[i] = '.';
689  	        }
690  	    }
691  	
692  	    for (id += len; *id != '\0'; id += len) {
693  	        valid = pcmk__xml_is_name_char(id, &len);
694  	        CRM_CHECK(len > 0, return); // UTF-8 encoding error
695  	        if (!valid) {
696  	            for (int i = 0; i < len; i++) {
697  	                id[i] = '.';
698  	            }
699  	        }
700  	    }
701  	}
702  	
703  	/*!
704  	 * \internal
705  	 * \brief Free an XML tree without ACL checks or change tracking
706  	 *
707  	 * \param[in,out] xml  XML node to free
708  	 */
709  	void
710  	pcmk__xml_free_node(xmlNode *xml)
711  	{
712  	    pcmk__xml_free_private_data(xml);
713  	    xmlUnlinkNode(xml);
714  	    xmlFreeNode(xml);
715  	}
716  	
717  	/*!
718  	 * \internal
719  	 * \brief Free an XML tree if ACLs allow; track deletion if tracking is enabled
720  	 *
721  	 * If \p node is the root of its document, free the entire document.
722  	 *
723  	 * \param[in,out] node      XML node to free
724  	 * \param[in]     position  Position of \p node among its siblings for change
725  	 *                          tracking (negative to calculate automatically if
726  	 *                          needed)
727  	 *
728  	 * \return Standard Pacemaker return code
729  	 */
730  	static int
731  	free_xml_with_position(xmlNode *node, int position)
732  	{
733  	    xmlDoc *doc = NULL;
734  	    xml_node_private_t *nodepriv = NULL;
735  	    xml_doc_private_t *docpriv = NULL;
736  	    GString *xpath = NULL;
737  	
738  	    if (node == NULL) {
739  	        return pcmk_rc_ok;
740  	    }
741  	    doc = node->doc;
742  	    nodepriv = node->_private;
743  	
744  	    if ((doc != NULL) && (xmlDocGetRootElement(doc) == node)) {
745  	        /* @TODO Should we check ACLs first? Otherwise it seems like we could
746  	         * free the root element without write permission.
747  	         */
748  	        pcmk__xml_free_doc(doc);
749  	        return pcmk_rc_ok;
750  	    }
751  	
752  	    if (!pcmk__check_acl(node, NULL, pcmk__xf_acl_write)) {
753  	        pcmk__if_tracing(
754  	            {
755  	                GString *xpath = pcmk__element_xpath(node);
756  	
757  	                qb_log_from_external_source(__func__, __FILE__,
758  	                                            "Cannot remove %s %x", LOG_TRACE,
759  	                                            __LINE__, 0, xpath->str,
760  	                                            nodepriv->flags);
761  	                g_string_free(xpath, TRUE);
762  	            },
763  	            {}
764  	        );
765  	        return EACCES;
766  	    }
767  	
768  	    if (!pcmk__xml_doc_all_flags_set(node->doc, pcmk__xf_tracking)
769  	        || pcmk__is_set(nodepriv->flags, pcmk__xf_created)) {
770  	        pcmk__xml_free_node(node);
771  	        return pcmk_rc_ok;
772  	    }
773  	
774  	    pcmk__assert(doc != NULL);
775  	
776  	    docpriv = doc->_private;
777  	    xpath = pcmk__element_xpath(node);
778  	
779  	    if (xpath != NULL) {
780  	        pcmk__deleted_xml_t *deleted_obj = NULL;
781  	
782  	        pcmk__trace("Deleting %s %p from %p", xpath->str, node, doc);
783  	
784  	        deleted_obj = pcmk__assert_alloc(1, sizeof(pcmk__deleted_xml_t));
785  	        deleted_obj->path = g_string_free(xpath, FALSE);
786  	        deleted_obj->position = -1;
787  	
788  	        // Record the position only for XML comments for now
789  	        if (node->type == XML_COMMENT_NODE) {
790  	            if (position >= 0) {
791  	                deleted_obj->position = position;
792  	
793  	            } else {
794  	                deleted_obj->position = pcmk__xml_position(node, pcmk__xf_skip);
795  	            }
796  	        }
797  	
798  	        docpriv->deleted_objs = g_list_append(docpriv->deleted_objs,
799  	                                              deleted_obj);
800  	        pcmk__xml_doc_set_flags(node->doc, pcmk__xf_dirty);
801  	    }
802  	
803  	    pcmk__xml_free_node(node);
804  	    return pcmk_rc_ok;
805  	}
806  	
807  	/*!
808  	 * \internal
809  	 * \brief Free an XML tree if ACLs allow; track deletion if tracking is enabled
810  	 *
811  	 * If \p xml is the root of its document, free the entire document.
812  	 *
813  	 * \param[in,out] xml  XML node to free
814  	 */
815  	void
816  	pcmk__xml_free(xmlNode *xml)
817  	{
818  	    free_xml_with_position(xml, -1);
819  	}
820  	
821  	/*!
822  	 * \internal
823  	 * \brief Make a deep copy of an XML node under a given parent
824  	 *
825  	 * \param[in,out] parent  XML element that will be the copy's parent (\c NULL
826  	 *                        to create a new XML document with the copy as root)
827  	 * \param[in]     src     XML node to copy
828  	 *
829  	 * \return Deep copy of \p src, or \c NULL if \p src is \c NULL
830  	 */
831  	xmlNode *
832  	pcmk__xml_copy(xmlNode *parent, xmlNode *src)
833  	{
834  	    xmlNode *copy = NULL;
835  	
836  	    if (src == NULL) {
837  	        return NULL;
838  	    }
839  	
840  	    if (parent == NULL) {
841  	        xmlDoc *doc = NULL;
842  	
843  	        // The copy will be the root element of a new document
844  	        pcmk__assert(src->type == XML_ELEMENT_NODE);
845  	
846  	        doc = pcmk__xml_new_doc();
847  	        copy = xmlDocCopyNode(src, doc, 1);
848  	        pcmk__mem_assert(copy);
849  	
850  	        xmlDocSetRootElement(doc, copy);
851  	
852  	    } else {
853  	        copy = xmlDocCopyNode(src, parent->doc, 1);
854  	        pcmk__mem_assert(copy);
855  	
856  	        xmlAddChild(parent, copy);
857  	    }
858  	
859  	    pcmk__xml_new_private_data(copy);
860  	    return copy;
861  	}
862  	
863  	/*!
864  	 * \internal
865  	 * \brief Replace one XML node with a copy of another XML node
866  	 *
867  	 * This function handles change tracking and applies ACLs.
868  	 *
869  	 * \param[in,out] old  XML node to replace
870  	 * \param[in]     new  XML node to copy as replacement for \p old
871  	 *
872  	 * \return Copy of \p new that replaced \p old
873  	 *
874  	 * \note This frees \p old.
875  	 * \note The caller is responsible for freeing the return value using
876  	 *       \c pcmk__xml_free() (but note that it may be part of a larger XML
877  	 *       tree).
878  	 */
879  	xmlNode *
880  	pcmk__xml_replace_with_copy(xmlNode *old, xmlNode *new)
881  	{
882  	    xmlNode *new_copy = NULL;
883  	
884  	    pcmk__assert((old != NULL) && (new != NULL));
885  	
886  	    /* Pass old to pcmk__xml_copy() so that new_copy gets created within the
887  	     * same doc. But old won't remain its parent.
888  	     */
889  	    new_copy = pcmk__xml_copy(old, new);
890  	    old = xmlReplaceNode(old, new_copy);
891  	
892  	    // old == NULL means memory allocation error
893  	    pcmk__assert(old != NULL);
894  	
895  	    // May be unnecessary but avoids slight changes to some test outputs
896  	    pcmk__xml_tree_foreach(new_copy, pcmk__xml_reset_node_flags, NULL);
897  	
898  	    if (pcmk__xml_doc_all_flags_set(new_copy->doc, pcmk__xf_tracking)) {
899  	        // Replaced sections may have included relevant ACLs
900  	        pcmk__apply_acls(new_copy->doc);
901  	    }
902  	    pcmk__xml_mark_changes(old, new_copy);
903  	    pcmk__xml_free_node(old);
904  	
905  	    return new_copy;
906  	}
907  	
908  	/*!
909  	 * \internal
910  	 * \brief Remove XML text nodes from specified XML and all its children
911  	 *
912  	 * \param[in,out] xml  XML to strip text from
913  	 */
914  	void
915  	pcmk__strip_xml_text(xmlNode *xml)
916  	{
917  	    xmlNode *iter = xml->children;
918  	
919  	    while (iter) {
920  	        xmlNode *next = iter->next;
921  	
922  	        switch (iter->type) {
923  	            case XML_TEXT_NODE:
924  	                pcmk__xml_free_node(iter);
925  	                break;
926  	
927  	            case XML_ELEMENT_NODE:
928  	                /* Search it */
929  	                pcmk__strip_xml_text(iter);
930  	                break;
931  	
932  	            default:
933  	                /* Leave it */
934  	                break;
935  	        }
936  	
937  	        iter = next;
938  	    }
939  	}
940  	
941  	/*!
942  	 * \internal
943  	 * \brief Check whether a string has XML special characters that must be escaped
944  	 *
945  	 * See \c pcmk__xml_escape() and \c pcmk__xml_escape_type for more details.
946  	 *
947  	 * \param[in] text  String to check
948  	 * \param[in] type  Type of escaping
949  	 *
950  	 * \return \c true if \p text has special characters that need to be escaped, or
951  	 *         \c false otherwise
952  	 */
953  	bool
954  	pcmk__xml_needs_escape(const char *text, enum pcmk__xml_escape_type type)
955  	{
956  	    if (text == NULL) {
957  	        return false;
958  	    }
959  	
960  	    while (*text != '\0') {
961  	        switch (type) {
962  	            case pcmk__xml_escape_text:
963  	                switch (*text) {
964  	                    case '<':
965  	                    case '>':
966  	                    case '&':
967  	                        return true;
968  	                    case '\n':
969  	                    case '\t':
970  	                        break;
971  	                    default:
972  	                        if (g_ascii_iscntrl(*text)) {
973  	                            return true;
974  	                        }
975  	                        break;
976  	                }
977  	                break;
978  	
979  	            case pcmk__xml_escape_attr:
980  	                switch (*text) {
981  	                    case '<':
982  	                    case '>':
983  	                    case '&':
984  	                    case '"':
985  	                        return true;
986  	                    default:
987  	                        if (g_ascii_iscntrl(*text)) {
988  	                            return true;
989  	                        }
990  	                        break;
991  	                }
992  	                break;
993  	
994  	            case pcmk__xml_escape_attr_pretty:
995  	                switch (*text) {
996  	                    case '\n':
997  	                    case '\r':
998  	                    case '\t':
999  	                    case '"':
1000 	                        return true;
1001 	                    default:
1002 	                        break;
1003 	                }
1004 	                break;
1005 	
1006 	            default:    // Invalid enum value
1007 	                pcmk__assert(false);
1008 	                break;
1009 	        }
1010 	
1011 	        text = g_utf8_next_char(text);
1012 	    }
1013 	    return false;
1014 	}
1015 	
1016 	/*!
1017 	 * \internal
1018 	 * \brief Replace special characters with their XML escape sequences
1019 	 *
1020 	 * \param[in] text  Text to escape
1021 	 * \param[in] type  Type of escaping
1022 	 *
1023 	 * \return Newly allocated string equivalent to \p text but with special
1024 	 *         characters replaced with XML escape sequences (or \c NULL if \p text
1025 	 *         is \c NULL). If \p text is not \c NULL, the return value is
1026 	 *         guaranteed not to be \c NULL.
1027 	 *
1028 	 * \note There are libxml functions that purport to do this:
1029 	 *       \c xmlEncodeEntitiesReentrant() and \c xmlEncodeSpecialChars().
1030 	 *       However, their escaping is incomplete. See:
1031 	 *       https://discourse.gnome.org/t/intended-use-of-xmlencodeentitiesreentrant-vs-xmlencodespecialchars/19252
1032 	 * \note The caller is responsible for freeing the return value using
1033 	 *       \c g_free().
1034 	 */
1035 	gchar *
1036 	pcmk__xml_escape(const char *text, enum pcmk__xml_escape_type type)
1037 	{
1038 	    GString *copy = NULL;
1039 	
1040 	    if (text == NULL) {
1041 	        return NULL;
1042 	    }
1043 	    copy = g_string_sized_new(strlen(text));
1044 	
1045 	    while (*text != '\0') {
1046 	        // Don't escape any non-ASCII characters
1047 	        if ((*text & 0x80) != 0) {
1048 	            size_t bytes = g_utf8_next_char(text) - text;
1049 	
1050 	            g_string_append_len(copy, text, bytes);
1051 	            text += bytes;
1052 	            continue;
1053 	        }
1054 	
1055 	        switch (type) {
1056 	            case pcmk__xml_escape_text:
1057 	                switch (*text) {
1058 	                    case '<':
1059 	                        g_string_append(copy, PCMK__XML_ENTITY_LT);
1060 	                        break;
1061 	                    case '>':
1062 	                        g_string_append(copy, PCMK__XML_ENTITY_GT);
1063 	                        break;
1064 	                    case '&':
1065 	                        g_string_append(copy, PCMK__XML_ENTITY_AMP);
1066 	                        break;
1067 	                    case '\n':
1068 	                    case '\t':
1069 	                        g_string_append_c(copy, *text);
1070 	                        break;
1071 	                    default:
1072 	                        if (g_ascii_iscntrl(*text)) {
1073 	                            g_string_append_printf(copy, "&#x%.2X;", *text);
1074 	                        } else {
1075 	                            g_string_append_c(copy, *text);
1076 	                        }
1077 	                        break;
1078 	                }
1079 	                break;
1080 	
1081 	            case pcmk__xml_escape_attr:
1082 	                switch (*text) {
1083 	                    case '<':
1084 	                        g_string_append(copy, PCMK__XML_ENTITY_LT);
1085 	                        break;
1086 	                    case '>':
1087 	                        g_string_append(copy, PCMK__XML_ENTITY_GT);
1088 	                        break;
1089 	                    case '&':
1090 	                        g_string_append(copy, PCMK__XML_ENTITY_AMP);
1091 	                        break;
1092 	                    case '"':
1093 	                        g_string_append(copy, PCMK__XML_ENTITY_QUOT);
1094 	                        break;
1095 	                    default:
1096 	                        if (g_ascii_iscntrl(*text)) {
1097 	                            g_string_append_printf(copy, "&#x%.2X;", *text);
1098 	                        } else {
1099 	                            g_string_append_c(copy, *text);
1100 	                        }
1101 	                        break;
1102 	                }
1103 	                break;
1104 	
1105 	            case pcmk__xml_escape_attr_pretty:
1106 	                switch (*text) {
1107 	                    case '"':
1108 	                        g_string_append(copy, "\\\"");
1109 	                        break;
1110 	                    case '\n':
1111 	                        g_string_append(copy, "\\n");
1112 	                        break;
1113 	                    case '\r':
1114 	                        g_string_append(copy, "\\r");
1115 	                        break;
1116 	                    case '\t':
1117 	                        g_string_append(copy, "\\t");
1118 	                        break;
1119 	                    default:
1120 	                        g_string_append_c(copy, *text);
1121 	                        break;
1122 	                }
1123 	                break;
1124 	
1125 	            default:    // Invalid enum value
1126 	                pcmk__assert(false);
1127 	                break;
1128 	        }
1129 	
1130 	        text = g_utf8_next_char(text);
1131 	    }
1132 	    return g_string_free(copy, FALSE);
1133 	}
1134 	
1135 	/*!
1136 	 * \internal
1137 	 * \brief Add an XML attribute to a node, marked as deleted
1138 	 *
1139 	 * When calculating XML changes, we need to know when an attribute has been
1140 	 * deleted. Add the attribute back to the new XML, so that we can check the
1141 	 * removal against ACLs, and mark it as deleted for later removal after
1142 	 * differences have been calculated.
1143 	 *
1144 	 * \param[in,out] new_xml     XML to modify
1145 	 * \param[in]     element     Name of XML element that changed (for logging)
1146 	 * \param[in]     attr_name   Name of attribute that was deleted
1147 	 * \param[in]     old_value   Value of attribute that was deleted
1148 	 */
1149 	static void
1150 	mark_attr_deleted(xmlNode *new_xml, const char *element, const char *attr_name,
1151 	                  const char *old_value)
1152 	{
1153 	    xml_doc_private_t *docpriv = new_xml->doc->_private;
1154 	    xmlAttr *attr = NULL;
1155 	    xml_node_private_t *nodepriv;
1156 	
1157 	    /* Restore the old value (without setting dirty flag recursively upwards or
1158 	     * checking ACLs)
1159 	     */
1160 	    pcmk__clear_xml_flags(docpriv, pcmk__xf_tracking);
1161 	    pcmk__xe_set(new_xml, attr_name, old_value);
1162 	    pcmk__set_xml_flags(docpriv, pcmk__xf_tracking);
1163 	
1164 	    // Reset flags (so the attribute doesn't appear as newly created)
1165 	    attr = xmlHasProp(new_xml, (const xmlChar *) attr_name);
1166 	    nodepriv = attr->_private;
1167 	    nodepriv->flags = 0;
1168 	
1169 	    // Check ACLs and mark restored value for later removal
1170 	    pcmk__xa_remove(attr, false);
1171 	
1172 	    pcmk__trace("XML attribute %s=%s was removed from %s", attr_name, old_value,
1173 	                element);
1174 	}
1175 	
1176 	/*
1177 	 * \internal
1178 	 * \brief Check ACLs for a changed XML attribute
1179 	 */
1180 	static void
1181 	mark_attr_changed(xmlNode *new_xml, const char *element, const char *attr_name,
1182 	                  const char *old_value)
1183 	{
1184 	    xml_doc_private_t *docpriv = new_xml->doc->_private;
1185 	    char *vcopy = pcmk__xe_get_copy(new_xml, attr_name);
1186 	
1187 	    pcmk__trace("XML attribute %s was changed from '%s' to '%s' in %s",
1188 	                attr_name, old_value, vcopy, element);
1189 	
1190 	    // Restore the original value (without checking ACLs)
1191 	    pcmk__clear_xml_flags(docpriv, pcmk__xf_tracking);
1192 	    pcmk__xe_set(new_xml, attr_name, old_value);
1193 	    pcmk__set_xml_flags(docpriv, pcmk__xf_tracking);
1194 	
1195 	    // Change it back to the new value, to check ACLs
1196 	    pcmk__xe_set(new_xml, attr_name, vcopy);
1197 	    free(vcopy);
1198 	}
1199 	
1200 	/*!
1201 	 * \internal
1202 	 * \brief Mark an XML attribute as having changed position
1203 	 *
1204 	 * \param[in,out] new_xml     XML to modify
1205 	 * \param[in]     element     Name of XML element that changed (for logging)
1206 	 * \param[in,out] old_attr    Attribute that moved, in original XML
1207 	 * \param[in,out] new_attr    Attribute that moved, in \p new_xml
1208 	 * \param[in]     p_old       Ordinal position of \p old_attr in original XML
1209 	 * \param[in]     p_new       Ordinal position of \p new_attr in \p new_xml
1210 	 */
1211 	static void
1212 	mark_attr_moved(xmlNode *new_xml, const char *element, xmlAttr *old_attr,
1213 	                xmlAttr *new_attr, int p_old, int p_new)
1214 	{
1215 	    xml_node_private_t *nodepriv = new_attr->_private;
1216 	
1217 	    pcmk__trace("XML attribute %s moved from position %d to %d in %s",
1218 	                old_attr->name, p_old, p_new, element);
1219 	
1220 	    // Mark document, element, and all element's parents as changed
1221 	    pcmk__mark_xml_node_dirty(new_xml);
1222 	
1223 	    // Mark attribute as changed
1224 	    pcmk__set_xml_flags(nodepriv, pcmk__xf_dirty|pcmk__xf_moved);
1225 	
1226 	    nodepriv = (p_old > p_new)? old_attr->_private : new_attr->_private;
1227 	    pcmk__set_xml_flags(nodepriv, pcmk__xf_skip);
1228 	}
1229 	
1230 	/*!
1231 	 * \internal
1232 	 * \brief Calculate differences in all previously existing XML attributes
1233 	 *
1234 	 * \param[in,out] old_xml  Original XML to compare
1235 	 * \param[in,out] new_xml  New XML to compare
1236 	 */
1237 	static void
1238 	xml_diff_old_attrs(xmlNode *old_xml, xmlNode *new_xml)
1239 	{
1240 	    xmlAttr *attr_iter = pcmk__xe_first_attr(old_xml);
1241 	
1242 	    while (attr_iter != NULL) {
1243 	        const char *name = (const char *) attr_iter->name;
1244 	        xmlAttr *old_attr = attr_iter;
1245 	        xmlAttr *new_attr = xmlHasProp(new_xml, attr_iter->name);
1246 	        const char *old_value = pcmk__xml_attr_value(attr_iter);
1247 	
1248 	        attr_iter = attr_iter->next;
1249 	        if (new_attr == NULL) {
1250 	            mark_attr_deleted(new_xml, (const char *) old_xml->name, name,
1251 	                              old_value);
1252 	
1253 	        } else {
1254 	            xml_node_private_t *nodepriv = new_attr->_private;
1255 	            int new_pos = pcmk__xml_position((xmlNode*) new_attr,
1256 	                                             pcmk__xf_skip);
1257 	            int old_pos = pcmk__xml_position((xmlNode*) old_attr,
1258 	                                             pcmk__xf_skip);
1259 	            const char *new_value = pcmk__xe_get(new_xml, name);
1260 	
1261 	            // This attribute isn't new
1262 	            pcmk__clear_xml_flags(nodepriv, pcmk__xf_created);
1263 	
1264 	            if (strcmp(new_value, old_value) != 0) {
1265 	                mark_attr_changed(new_xml, (const char *) old_xml->name, name,
1266 	                                  old_value);
1267 	
1268 	            } else if ((old_pos != new_pos)
1269 	                       && !pcmk__xml_doc_all_flags_set(new_xml->doc,
1270 	                                                       pcmk__xf_ignore_attr_pos
1271 	                                                       |pcmk__xf_tracking)) {
1272 	                /* pcmk__xf_tracking is always set by pcmk__xml_mark_changes()
1273 	                 * before this function is called, so only the
1274 	                 * pcmk__xf_ignore_attr_pos check is truly relevant.
1275 	                 */
1276 	                mark_attr_moved(new_xml, (const char *) old_xml->name,
1277 	                                old_attr, new_attr, old_pos, new_pos);
1278 	            }
1279 	        }
1280 	    }
1281 	}
1282 	
1283 	/*!
1284 	 * \internal
1285 	 * \brief Check all attributes in new XML for creation
1286 	 *
1287 	 * For each of a given XML element's attributes marked as newly created, accept
1288 	 * (and mark as dirty) or reject the creation according to ACLs.
1289 	 *
1290 	 * \param[in,out] new_xml  XML to check
1291 	 */
1292 	static void
1293 	mark_created_attrs(xmlNode *new_xml)
1294 	{
1295 	    xmlAttr *attr_iter = pcmk__xe_first_attr(new_xml);
1296 	
1297 	    while (attr_iter != NULL) {
1298 	        xmlAttr *new_attr = attr_iter;
1299 	        xml_node_private_t *nodepriv = attr_iter->_private;
1300 	
1301 	        attr_iter = attr_iter->next;
1302 	        if (pcmk__is_set(nodepriv->flags, pcmk__xf_created)) {
1303 	            const char *attr_name = (const char *) new_attr->name;
1304 	
1305 	            pcmk__trace("Created new attribute %s=%s in %s", attr_name,
1306 	                        pcmk__xml_attr_value(new_attr), new_xml->name);
1307 	
1308 	            /* Check ACLs (we can't use the remove-then-create trick because it
1309 	             * would modify the attribute position).
1310 	             */
1311 	            if (pcmk__check_acl(new_xml, attr_name, pcmk__xf_acl_write)) {
1312 	                pcmk__mark_xml_attr_dirty(new_attr);
1313 	            } else {
1314 	                // Creation was not allowed, so remove the attribute
1315 	                pcmk__xa_remove(new_attr, true);
1316 	            }
1317 	        }
1318 	    }
1319 	}
1320 	
1321 	/*!
1322 	 * \internal
1323 	 * \brief Calculate differences in attributes between two XML nodes
1324 	 *
1325 	 * \param[in,out] old_xml  Original XML to compare
1326 	 * \param[in,out] new_xml  New XML to compare
1327 	 */
1328 	static void
1329 	xml_diff_attrs(xmlNode *old_xml, xmlNode *new_xml)
1330 	{
1331 	    // Cleared later if attributes are not really new
1332 	    for (xmlAttr *attr = pcmk__xe_first_attr(new_xml); attr != NULL;
1333 	         attr = attr->next) {
1334 	        xml_node_private_t *nodepriv = attr->_private;
1335 	
1336 	        pcmk__set_xml_flags(nodepriv, pcmk__xf_created);
1337 	    }
1338 	
1339 	    xml_diff_old_attrs(old_xml, new_xml);
1340 	    mark_created_attrs(new_xml);
1341 	}
1342 	
1343 	/*!
1344 	 * \internal
1345 	 * \brief Add a deleted object record for an old XML child if ACLs allow
1346 	 *
1347 	 * This is intended to be called for a child of an old XML element that is not
1348 	 * present as a child of a new XML element.
1349 	 *
1350 	 * Add a temporary copy of the old child to the new XML. Then check whether ACLs
1351 	 * would have allowed the deletion of that element. If so, add a deleted object
1352 	 * record for it to the new XML's document, and set the \c pcmk__xf_skip flag on
1353 	 * the old child.
1354 	 *
1355 	 * The temporary copy is removed before returning. The new XML and all of its
1356 	 * ancestors will have the \c pcmk__xf_dirty flag set because of the creation,
1357 	 * however.
1358 	 *
1359 	 * \param[in,out] old_child   Child of old XML
1360 	 * \param[in,out] new_parent  New XML that does not contain \p old_child
1361 	 *
1362 	 * \note The deletion is checked using the new XML's ACLs. The ACLs may have
1363 	 *       also changed between the old and new XML trees. Callers should take
1364 	 *       reasonable action if there were ACL changes that themselves would have
1365 	 *       been denied.
1366 	 */
1367 	static void
1368 	mark_child_deleted(xmlNode *old_child, xmlNode *new_parent)
1369 	{
1370 	    int pos = pcmk__xml_position(old_child, pcmk__xf_skip);
1371 	
1372 	    // Re-create the child element so we can check ACLs
1373 	    xmlNode *candidate = pcmk__xml_copy(new_parent, old_child);
1374 	
1375 	    // Clear flags on new child and its children
1376 	    pcmk__xml_tree_foreach(candidate, pcmk__xml_reset_node_flags, NULL);
1377 	
1378 	    // free_xml_with_position() will check whether ACLs allow the deletion
1379 	    pcmk__apply_acls(candidate->doc);
1380 	
1381 	    /* Try to remove the child again (which will track it in document's
1382 	     * deleted_objs on success)
1383 	     */
1384 	    if (free_xml_with_position(candidate, pos) != pcmk_rc_ok) {
1385 	        // ACLs denied deletion in free_xml_with_position. Free candidate here.
1386 	        pcmk__xml_free_node(candidate);
1387 	    }
1388 	
1389 	    pcmk__set_xml_flags((xml_node_private_t *) old_child->_private,
1390 	                        pcmk__xf_skip);
1391 	}
1392 	
1393 	/*!
1394 	 * \internal
1395 	 * \brief Mark a new child as moved and set \c pcmk__xf_skip as appropriate
1396 	 *
1397 	 * \param[in,out] old_child  Child of old XML
1398 	 * \param[in,out] new_child  Child of new XML that matches \p old_child
1399 	 * \param[in]     old_pos    Position of \p old_child among its siblings
1400 	 * \param[in]     new_pos    Position of \p new_child among its siblings
1401 	 */
1402 	static void
1403 	mark_child_moved(xmlNode *old_child, xmlNode *new_child, int old_pos,
1404 	                 int new_pos)
1405 	{
1406 	    const char *id_s = pcmk__s(pcmk__xe_id(new_child), "<no id>");
1407 	    xmlNode *new_parent = new_child->parent;
1408 	    xml_node_private_t *nodepriv = new_child->_private;
1409 	
1410 	    pcmk__trace("Child element %s with " PCMK_XA_ID "='%s' moved from position "
1411 	                "%d to %d under %s",
1412 	                new_child->name, id_s, old_pos, new_pos, new_parent->name);
1413 	    pcmk__mark_xml_node_dirty(new_parent);
1414 	    pcmk__set_xml_flags(nodepriv, pcmk__xf_moved);
1415 	
1416 	    /* @TODO Figure out and document why we skip the old child in future
1417 	     * position calculations if the old position is higher, and skip the new
1418 	     * child in future position calculations if the new position is higher. This
1419 	     * goes back to d028b52, and there's no explanation in the commit message.
1420 	     */
1421 	    if (old_pos > new_pos) {
1422 	        nodepriv = old_child->_private;
1423 	    }
1424 	    pcmk__set_xml_flags(nodepriv, pcmk__xf_skip);
1425 	}
1426 	
1427 	/*!
1428 	 * \internal
1429 	 * \brief Check whether a new XML child comment matches an old XML child comment
1430 	 *
1431 	 * Two comments match if they have the same position among their siblings and
1432 	 * the same contents.
1433 	 *
1434 	 * If \p new_comment has the \c pcmk__xf_skip flag set, then it is automatically
1435 	 * considered not to match.
1436 	 *
1437 	 * \param[in] old_comment  Old XML child element
1438 	 * \param[in] new_comment  New XML child element
1439 	 *
1440 	 * \retval \c true   if \p new_comment matches \p old_comment
1441 	 * \retval \c false  otherwise
1442 	 */
1443 	static bool
1444 	new_comment_matches(const xmlNode *old_comment, const xmlNode *new_comment)
1445 	{
1446 	    xml_node_private_t *nodepriv = new_comment->_private;
1447 	
1448 	    if (pcmk__is_set(nodepriv->flags, pcmk__xf_skip)) {
1449 	        /* @TODO Should we also return false if old_comment has pcmk__xf_skip
1450 	         * set? This preserves existing behavior at time of writing.
1451 	         */
1452 	        return false;
1453 	    }
1454 	    if (pcmk__xml_position(old_comment, pcmk__xf_skip)
1455 	        != pcmk__xml_position(new_comment, pcmk__xf_skip)) {
1456 	        return false;
1457 	    }
1458 	    return pcmk__xc_matches(old_comment, new_comment);
1459 	}
1460 	
1461 	/*!
1462 	 * \internal
1463 	 * \brief Check whether a new XML child element matches an old XML child element
1464 	 *
1465 	 * Two elements match if they have the same name and the same ID. (Both IDs can
1466 	 * be \c NULL.)
1467 	 *
1468 	 * For XML attributes other than \c PCMK_XA_ID, we can treat a value change as
1469 	 * an in-place modification. However, when Pacemaker applies a patchset, it uses
1470 	 * the \c PCMK_XA_ID attribute to find the node to update (modify, delete, or
1471 	 * move). If we treat two nodes with different \c PCMK_XA_ID attributes as
1472 	 * matching and then mark that attribute as changed, it can cause this lookup to
1473 	 * fail.
1474 	 *
1475 	 * There's unlikely to ever be much practical reason to treat elements with
1476 	 * different IDs as a change. Unless that changes, we'll treat them as a
1477 	 * mismatch.
1478 	 *
1479 	 * \param[in] old_element  Old XML child element
1480 	 * \param[in] new_element  New XML child element
1481 	 *
1482 	 * \retval \c true   if \p new_element matches \p old_element
1483 	 * \retval \c false  otherwise
1484 	 */
1485 	static bool
1486 	new_element_matches(const xmlNode *old_element, const xmlNode *new_element)
1487 	{
1488 	    return pcmk__xe_is(new_element, (const char *) old_element->name)
1489 	           && pcmk__str_eq(pcmk__xe_id(old_element), pcmk__xe_id(new_element),
1490 	                           pcmk__str_none);
1491 	}
1492 	
1493 	/*!
1494 	 * \internal
1495 	 * \brief Check whether a new XML child node matches an old XML child node
1496 	 *
1497 	 * Node types must be the same in order to match.
1498 	 *
1499 	 * For comments, a match is a comment at the same position with the same
1500 	 * content.
1501 	 *
1502 	 * For elements, a match is an element with the same name and the same ID. (Both
1503 	 * IDs can be \c NULL.)
1504 	 *
1505 	 * For other node types, there is no match.
1506 	 *
1507 	 * \param[in] old_child  Child of old XML
1508 	 * \param[in] new_child  Child of new XML
1509 	 *
1510 	 * \retval \c true   if \p new_child matches \p old_child
1511 	 * \retval \c false  otherwise
1512 	 */
1513 	static bool
1514 	new_child_matches(const xmlNode *old_child, const xmlNode *new_child)
1515 	{
1516 	    if (old_child->type != new_child->type) {
1517 	        return false;
1518 	    }
1519 	
1520 	    switch (old_child->type) {
1521 	        case XML_COMMENT_NODE:
1522 	            return new_comment_matches(old_child, new_child);
1523 	        case XML_ELEMENT_NODE:
1524 	            return new_element_matches(old_child, new_child);
1525 	        default:
1526 	            return false;
1527 	    }
1528 	}
1529 	
1530 	/*!
1531 	 * \internal
1532 	 * \brief Find matching XML node pairs between old and new XML's children
1533 	 *
1534 	 * A node that is part of a matching pair gets its <tt>_private:match</tt>
1535 	 * member set to the matching node.
1536 	 *
1537 	 * \param[in,out] old_xml       Old XML
1538 	 * \param[in,out] new_xml       New XML
1539 	 */
1540 	static void
1541 	find_matching_children(xmlNode *old_xml, xmlNode *new_xml)
1542 	{
1543 	    for (xmlNode *old_child = pcmk__xml_first_child(old_xml); old_child != NULL;
1544 	         old_child = pcmk__xml_next(old_child)) {
1545 	
1546 	        xml_node_private_t *old_nodepriv = old_child->_private;
1547 	
1548 	        if ((old_nodepriv == NULL) || (old_nodepriv->match != NULL)) {
1549 	            // Can't process, or we already found a match for this old child
1550 	            continue;
1551 	        }
1552 	
1553 	        for (xmlNode *new_child = pcmk__xml_first_child(new_xml);
1554 	             new_child != NULL; new_child = pcmk__xml_next(new_child)) {
1555 	
1556 	            xml_node_private_t *new_nodepriv = new_child->_private;
1557 	
1558 	            if ((new_nodepriv == NULL) || (new_nodepriv->match != NULL)) {
1559 	                /* Can't process, or this new child already matched some old
1560 	                 * child
1561 	                 */
1562 	                continue;
1563 	            }
1564 	
1565 	            if (new_child_matches(old_child, new_child)) {
1566 	                old_nodepriv->match = new_child;
1567 	                new_nodepriv->match = old_child;
1568 	                break;
1569 	            }
1570 	        }
1571 	    }
1572 	}
1573 	
1574 	/*!
1575 	 * \internal
1576 	 * \brief Mark changes between two XML trees
1577 	 *
1578 	 * Set flags in a new XML tree to indicate changes relative to an old XML tree.
1579 	 *
1580 	 * \param[in,out] old_xml  XML before changes
1581 	 * \param[in,out] new_xml  XML after changes
1582 	 *
1583 	 * \note This may set \c pcmk__xf_skip on parts of \p old_xml.
1584 	 */
1585 	void
1586 	pcmk__xml_mark_changes(xmlNode *old_xml, xmlNode *new_xml)
1587 	{
1588 	    /* This function may set the xml_node_private_t:match member on children of
1589 	     * old_xml and new_xml, but it clears that member before returning.
1590 	     *
1591 	     * @TODO Ensure we handle (for example, by copying) or reject user-created
1592 	     * XML that is missing xml_node_private_t at top level or in any children.
1593 	     * Similarly, check handling of node types for which we don't create private
1594 	     * data. For now, we'll skip them in the loops below.
1595 	     */
1596 	    CRM_CHECK((old_xml != NULL) && (new_xml != NULL), return);
1597 	    if ((old_xml->_private == NULL) || (new_xml->_private == NULL)) {
1598 	        return;
1599 	    }
1600 	
1601 	    pcmk__xml_doc_set_flags(new_xml->doc, pcmk__xf_tracking);
1602 	    xml_diff_attrs(old_xml, new_xml);
1603 	
1604 	    find_matching_children(old_xml, new_xml);
1605 	
1606 	    // Process matches (changed children) and deletions
1607 	    for (xmlNode *old_child = pcmk__xml_first_child(old_xml); old_child != NULL;
1608 	         old_child = pcmk__xml_next(old_child)) {
1609 	
1610 	        xml_node_private_t *nodepriv = old_child->_private;
1611 	        xmlNode *new_child = NULL;
1612 	
1613 	        if (nodepriv == NULL) {
1614 	            continue;
1615 	        }
1616 	
1617 	        if (nodepriv->match == NULL) {
1618 	            // No match in new XML means the old child was deleted
1619 	            mark_child_deleted(old_child, new_xml);
1620 	            continue;
1621 	        }
1622 	
1623 	        /* Fetch the match and clear old_child->_private's match member.
1624 	         * new_child->_private's match member is handled in the new_xml loop.
1625 	         */
1626 	        new_child = nodepriv->match;
1627 	        nodepriv->match = NULL;
1628 	
1629 	        pcmk__assert(old_child->type == new_child->type);
1630 	
1631 	        if (old_child->type == XML_COMMENT_NODE) {
1632 	            // Comments match only if their positions and contents match
1633 	            continue;
1634 	        }
1635 	
1636 	        pcmk__xml_mark_changes(old_child, new_child);
1637 	    }
1638 	
1639 	    /* Mark unmatched new children as created, and mark matched new children as
1640 	     * moved if their positions changed. Grab the next new child in advance,
1641 	     * since new_child may get freed in the loop body.
1642 	     */
1643 	    for (xmlNode *new_child = pcmk__xml_first_child(new_xml),
1644 	                 *next = pcmk__xml_next(new_child);
1645 	         new_child != NULL;
1646 	         new_child = next, next = pcmk__xml_next(new_child)) {
1647 	
1648 	        xml_node_private_t *nodepriv = new_child->_private;
1649 	
1650 	        if (nodepriv == NULL) {
1651 	            continue;
1652 	        }
1653 	
1654 	        if (nodepriv->match != NULL) {
1655 	            /* Fetch the match and clear new_child->_private's match member. Any
1656 	             * changes were marked in the old_xml loop. Mark the move.
1657 	             *
1658 	             * We might be able to mark the move earlier, when we mark changes
1659 	             * for matches in the old_xml loop, consolidating both actions. We'd
1660 	             * have to think about whether the timing of setting the
1661 	             * pcmk__xf_skip flag makes any difference.
1662 	             */
1663 	            xmlNode *old_child = nodepriv->match;
1664 	            int old_pos = pcmk__xml_position(old_child, pcmk__xf_skip);
1665 	            int new_pos = pcmk__xml_position(new_child, pcmk__xf_skip);
1666 	
1667 	            if (old_pos != new_pos) {
1668 	                mark_child_moved(old_child, new_child, old_pos, new_pos);
1669 	            }
1670 	            nodepriv->match = NULL;
1671 	            continue;
1672 	        }
1673 	
1674 	        // No match in old XML means the new child is newly created
1675 	        pcmk__set_xml_flags(nodepriv, pcmk__xf_skip);
1676 	        mark_xml_tree_dirty_created(new_child);
1677 	
1678 	        // Check whether creation was allowed (may free new_child)
1679 	        pcmk__apply_creation_acl(new_child, true);
1680 	    }
1681 	}
1682 	
1683 	char *
1684 	pcmk__xml_artefact_root(enum pcmk__xml_artefact_ns ns)
1685 	{
1686 	    static const char *base = NULL;
1687 	    char *ret = NULL;
1688 	
1689 	    if (base == NULL) {
1690 	        base = pcmk__env_option(PCMK__ENV_SCHEMA_DIRECTORY);
1691 	    }
1692 	    if (pcmk__str_empty(base)) {
1693 	        base = PCMK_SCHEMA_DIR;
1694 	    }
1695 	
1696 	    switch (ns) {
1697 	        case pcmk__xml_artefact_ns_legacy_rng:
1698 	        case pcmk__xml_artefact_ns_legacy_xslt:
1699 	            ret = strdup(base);
1700 	            break;
1701 	        case pcmk__xml_artefact_ns_base_rng:
1702 	        case pcmk__xml_artefact_ns_base_xslt:
1703 	            ret = pcmk__assert_asprintf("%s/base", base);
1704 	            break;
1705 	        default:
1706 	            pcmk__err("XML artefact family specified as %u not recognized", ns);
1707 	    }
1708 	    return ret;
1709 	}
1710 	
1711 	static char *
1712 	find_artefact(enum pcmk__xml_artefact_ns ns, const char *path, const char *filespec)
1713 	{
1714 	    char *ret = NULL;
1715 	
1716 	    switch (ns) {
1717 	        case pcmk__xml_artefact_ns_legacy_rng:
1718 	        case pcmk__xml_artefact_ns_base_rng:
1719 	            if (g_str_has_suffix(filespec, ".rng")) {
1720 	                ret = pcmk__assert_asprintf("%s/%s", path, filespec);
1721 	            } else {
1722 	                ret = pcmk__assert_asprintf("%s/%s.rng", path, filespec);
1723 	            }
1724 	            break;
1725 	        case pcmk__xml_artefact_ns_legacy_xslt:
1726 	        case pcmk__xml_artefact_ns_base_xslt:
1727 	            if (g_str_has_suffix(filespec, ".xsl")) {
1728 	                ret = pcmk__assert_asprintf("%s/%s", path, filespec);
1729 	            } else {
1730 	                ret = pcmk__assert_asprintf("%s/%s.xsl", path, filespec);
1731 	            }
1732 	            break;
1733 	        default:
1734 	            pcmk__err("XML artefact family specified as %u not recognized", ns);
1735 	    }
1736 	
1737 	    return ret;
1738 	}
1739 	
1740 	char *
1741 	pcmk__xml_artefact_path(enum pcmk__xml_artefact_ns ns, const char *filespec)
1742 	{
1743 	    struct stat sb;
1744 	    char *base = pcmk__xml_artefact_root(ns);
1745 	    char *ret = NULL;
1746 	
1747 	    ret = find_artefact(ns, base, filespec);
1748 	    free(base);
1749 	
1750 	    if (stat(ret, &sb) != 0 || !S_ISREG(sb.st_mode)) {
1751 	        const char *remote_schema_dir = pcmk__remote_schema_dir();
1752 	
1753 	        free(ret);
1754 	        ret = find_artefact(ns, remote_schema_dir, filespec);
1755 	    }
1756 	
1757 	    return ret;
1758 	}
1759 	
1760 	// Deprecated functions kept only for backward API compatibility
1761 	// LCOV_EXCL_START
1762 	
1763 	#include <libxml/parser.h>              // xmlCleanupParser()
1764 	
1765 	#include <crm/common/xml_compat.h>
1766 	
1767 	xmlNode *
1768 	copy_xml(xmlNode *src)
1769 	{
1770 	    xmlDoc *doc = pcmk__xml_new_doc();
1771 	    xmlNode *copy = NULL;
1772 	
1773 	    copy = xmlDocCopyNode(src, doc, 1);
1774 	    pcmk__mem_assert(copy);
1775 	
1776 	    xmlDocSetRootElement(doc, copy);
1777 	    pcmk__xml_new_private_data(copy);
1778 	    return copy;
1779 	}
1780 	
1781 	void
1782 	crm_xml_init(void)
1783 	{
1784 	    pcmk__schema_init();
1785 	}
1786 	
1787 	void
1788 	crm_xml_cleanup(void)
1789 	{
1790 	    pcmk__schema_cleanup();
1791 	    xmlCleanupParser();
1792 	}
1793 	
1794 	void
1795 	pcmk_free_xml_subtree(xmlNode *xml)
1796 	{
1797 	    pcmk__xml_free_node(xml);
1798 	}
1799 	
1800 	void
1801 	free_xml(xmlNode *child)
1802 	{
1803 	    pcmk__xml_free(child);
1804 	}
1805 	
1806 	void
1807 	crm_xml_sanitize_id(char *id)
1808 	{
1809 	    char *c;
1810 	
1811 	    for (c = id; *c; ++c) {
1812 	        switch (*c) {
1813 	            case ':':
1814 	            case '#':
1815 	                *c = '.';
1816 	        }
1817 	    }
1818 	}
1819 	
1820 	bool
1821 	xml_tracking_changes(xmlNode *xml)
1822 	{
1823 	    return (xml != NULL)
1824 	           && pcmk__xml_doc_all_flags_set(xml->doc, pcmk__xf_tracking);
1825 	}
1826 	
1827 	bool
1828 	xml_document_dirty(xmlNode *xml)
1829 	{
1830 	    return (xml != NULL)
1831 	           && pcmk__xml_doc_all_flags_set(xml->doc, pcmk__xf_dirty);
1832 	}
1833 	
1834 	void
1835 	xml_accept_changes(xmlNode *xml)
1836 	{
1837 	    if (xml != NULL) {
1838 	        pcmk__xml_commit_changes(xml->doc);
1839 	    }
1840 	}
1841 	
1842 	void
1843 	xml_track_changes(xmlNode *xml, const char *user, xmlNode *acl_source,
1844 	                  bool enforce_acls)
1845 	{
1846 	    if (xml == NULL) {
1847 	        return;
1848 	    }
1849 	
1850 	    pcmk__xml_commit_changes(xml->doc);
1851 	    pcmk__trace("Tracking changes%s to %p", (enforce_acls? " with ACLs" : ""),
1852 	                xml);
1853 	    pcmk__xml_doc_set_flags(xml->doc, pcmk__xf_tracking);
1854 	    if (enforce_acls) {
1855 	        if (acl_source == NULL) {
1856 	            acl_source = xml;
1857 	        }
1858 	        pcmk__xml_doc_set_flags(xml->doc, pcmk__xf_acl_enabled);
1859 	        pcmk__unpack_acls(acl_source->doc, xml->doc->_private, user);
1860 	        pcmk__apply_acls(xml->doc);
1861 	    }
1862 	}
1863 	
1864 	void
1865 	xml_calculate_changes(xmlNode *old_xml, xmlNode *new_xml)
1866 	{
1867 	    CRM_CHECK((old_xml != NULL) && (new_xml != NULL)
1868 	              && pcmk__xe_is(old_xml, (const char *) new_xml->name)
1869 	              && pcmk__str_eq(pcmk__xe_id(old_xml), pcmk__xe_id(new_xml),
1870 	                              pcmk__str_none),
1871 	              return);
1872 	
1873 	    if (!pcmk__xml_doc_all_flags_set(new_xml->doc, pcmk__xf_tracking)) {
1874 	        // Ensure tracking has a clean start (pcmk__xml_mark_changes() enables)
1875 	        pcmk__xml_commit_changes(new_xml->doc);
1876 	    }
1877 	
1878 	    pcmk__xml_mark_changes(old_xml, new_xml);
1879 	}
1880 	
1881 	void
1882 	xml_calculate_significant_changes(xmlNode *old_xml, xmlNode *new_xml)
1883 	{
1884 	    CRM_CHECK((old_xml != NULL) && (new_xml != NULL)
1885 	              && pcmk__xe_is(old_xml, (const char *) new_xml->name)
1886 	              && pcmk__str_eq(pcmk__xe_id(old_xml), pcmk__xe_id(new_xml),
1887 	                              pcmk__str_none),
1888 	              return);
1889 	
1890 	    /* BUG: If pcmk__xf_tracking is not set for new_xml when this function is
1891 	     * called, then we unset pcmk__xf_ignore_attr_pos via
1892 	     * pcmk__xml_commit_changes(). Since this function is about to be
1893 	     * deprecated, it's not worth fixing this and changing the user-facing
1894 	     * behavior.
1895 	     */
1896 	    pcmk__xml_doc_set_flags(new_xml->doc, pcmk__xf_ignore_attr_pos);
1897 	
1898 	    if (!pcmk__xml_doc_all_flags_set(new_xml->doc, pcmk__xf_tracking)) {
1899 	        // Ensure tracking has a clean start (pcmk__xml_mark_changes() enables)
1900 	        pcmk__xml_commit_changes(new_xml->doc);
1901 	    }
1902 	
1903 	    pcmk__xml_mark_changes(old_xml, new_xml);
1904 	}
1905 	
1906 	// LCOV_EXCL_STOP
1907 	// End deprecated API
1908