Coverage Report

Created: 2026-04-04 06:09

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