Coverage Report

Created: 2026-08-14 06:23

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/pacemaker/lib/common/xml_tracking.c
Line
Count
Source
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 <stdbool.h>
13
#include <stddef.h>             // NULL
14
#include <stdlib.h>             // free()
15
16
#include <libxml/tree.h>        // xmlNode, etc.
17
#include <libxml/xmlstring.h>   // xmlChar
18
19
#include "crmcommon_private.h"
20
21
/*!
22
 * \internal
23
 * \brief Set the \c pcmk__xf_created flag on an attribute
24
 *
25
 * \param[in,out] attr       XML attribute
26
 * \param[in]     user_data  Ignored
27
 *
28
 * \return \c true (to continue iterating)
29
 *
30
 * \note This is compatible with \c pcmk__xe_foreach_attr().
31
 */
32
static bool
33
mark_attr_created(xmlAttr *attr, void *user_data)
34
0
{
35
0
    xml_node_private_t *nodepriv = attr->_private;
36
37
0
    pcmk__set_xml_flags(nodepriv, pcmk__xf_created);
38
0
    return true;
39
0
}
40
41
/*!
42
 * \internal
43
 * \brief Add an XML attribute to a node, marked as deleted
44
 *
45
 * When calculating XML changes, we need to know when an attribute has been
46
 * deleted. Add the attribute back to the new XML, so that we can check the
47
 * removal against ACLs, and mark it as deleted for later removal after
48
 * differences have been calculated.
49
 *
50
 * \param[in,out] new_xml    XML to modify
51
 * \param[in]     attr_name  Name of attribute that was deleted
52
 * \param[in]     old_value  Value of attribute that was deleted
53
 */
54
static void
55
mark_attr_deleted(xmlNode *new_xml, const char *attr_name,
56
                  const char *old_value)
57
0
{
58
0
    xmlAttr *attr = NULL;
59
0
    xml_node_private_t *nodepriv;
60
61
    /* Restore the old value (without setting dirty flag recursively upwards or
62
     * checking ACLs)
63
     */
64
0
    pcmk__xml_doc_clear_flags(new_xml->doc, pcmk__xf_tracking);
65
0
    pcmk__xe_set(new_xml, attr_name, old_value);
66
0
    pcmk__xml_doc_set_flags(new_xml->doc, pcmk__xf_tracking);
67
68
    // Reset flags (so the attribute doesn't appear as newly created)
69
0
    attr = xmlHasProp(new_xml, (const xmlChar *) attr_name);
70
0
    nodepriv = attr->_private;
71
0
    nodepriv->flags = 0;
72
73
    // Check ACLs and mark restored value for later removal
74
0
    pcmk__xa_remove(attr, false);
75
76
0
    pcmk__trace("XML attribute %s=%s was removed from %s", attr_name, old_value,
77
0
                (const char *) new_xml->name);
78
0
}
79
80
/*
81
 * \internal
82
 * \brief Check ACLs for a changed XML attribute
83
 */
84
static void
85
mark_attr_changed(xmlNode *new_xml, const char *attr_name,
86
                  const char *old_value)
87
0
{
88
0
    char *vcopy = pcmk__xe_get_copy(new_xml, attr_name);
89
90
0
    pcmk__trace("XML attribute %s was changed from '%s' to '%s' in %s",
91
0
                attr_name, old_value, vcopy, (const char *) new_xml->name);
92
93
    // Restore the original value (without checking ACLs)
94
0
    pcmk__xml_doc_clear_flags(new_xml->doc, pcmk__xf_tracking);
95
0
    pcmk__xe_set(new_xml, attr_name, old_value);
96
0
    pcmk__xml_doc_set_flags(new_xml->doc, pcmk__xf_tracking);
97
98
    // Change it back to the new value, to check ACLs
99
0
    pcmk__xe_set(new_xml, attr_name, vcopy);
100
0
    free(vcopy);
101
0
}
102
103
/*!
104
 * \internal
105
 * \brief Mark an XML attribute as having changed position
106
 *
107
 * \param[in,out] new_xml   XML to modify
108
 * \param[in,out] old_attr  Attribute that moved, in original XML
109
 * \param[in,out] new_attr  Attribute that moved, in \p new_xml
110
 * \param[in]     p_old     Ordinal position of \p old_attr in original XML
111
 * \param[in]     p_new     Ordinal position of \p new_attr in \p new_xml
112
 */
113
static void
114
mark_attr_moved(xmlNode *new_xml, xmlAttr *old_attr, xmlAttr *new_attr,
115
                int p_old, int p_new)
116
0
{
117
0
    xml_node_private_t *nodepriv = new_attr->_private;
118
119
0
    pcmk__trace("XML attribute %s moved from position %d to %d in %s",
120
0
                old_attr->name, p_old, p_new, (const char *) new_xml->name);
121
122
    // Mark document, element, and all element's parents as changed
123
0
    pcmk__mark_xml_node_dirty(new_xml);
124
125
    // Mark attribute as changed
126
0
    pcmk__set_xml_flags(nodepriv, pcmk__xf_dirty|pcmk__xf_moved);
127
128
0
    nodepriv = (p_old > p_new)? old_attr->_private : new_attr->_private;
129
0
    pcmk__set_xml_flags(nodepriv, pcmk__xf_skip);
130
0
}
131
132
/*!
133
 * \internal
134
 * \brief Mark an XML attribute as deleted, changed, or moved if appropriate
135
 *
136
 * Given an attribute (from an old XML element) and a new XML element, check
137
 * whether the attribute has been deleted, changed, or moved between the old and
138
 * new elements. If so, mark the new XML element to indicate what changed.
139
 *
140
 * \param[in,out] old_attr   XML attribute from old element
141
 * \param[in,out] user_data  New XML element
142
 *
143
 * \return \c true (to continue iterating)
144
 *
145
 * \note This is compatible with \c pcmk__xe_foreach_attr().
146
 */
147
static bool
148
mark_attr_diff(xmlAttr *old_attr, void *user_data)
149
0
{
150
0
    xmlNode *new_xml = user_data;
151
152
0
    const char *name = (const char *) old_attr->name;
153
154
0
    xmlAttr *new_attr = xmlHasProp(new_xml, old_attr->name);
155
0
    xml_node_private_t *new_priv = NULL;
156
157
0
    const char *old_value = pcmk__xml_attr_value(old_attr);
158
0
    const char *new_value = NULL;
159
160
0
    int old_pos = 0;
161
0
    int new_pos = 0;
162
163
0
    if (new_attr == NULL) {
164
0
        mark_attr_deleted(new_xml, name, old_value);
165
0
        return true;
166
0
    }
167
168
0
    new_priv = new_attr->_private;
169
0
    new_value = pcmk__xe_get(new_xml, name);
170
171
    // This attribute isn't new
172
0
    pcmk__clear_xml_flags(new_priv, pcmk__xf_created);
173
174
0
    if (!pcmk__str_eq(old_value, new_value, pcmk__str_none)) {
175
0
        mark_attr_changed(new_xml, name, old_value);
176
0
        return true;
177
0
    }
178
179
0
    old_pos = pcmk__xml_position((xmlNode *) old_attr, pcmk__xf_skip);
180
0
    new_pos = pcmk__xml_position((xmlNode *) new_attr, pcmk__xf_skip);
181
182
0
    if ((old_pos == new_pos)
183
0
        || pcmk__xml_doc_all_flags_set(new_xml->doc,
184
0
                                       pcmk__xf_ignore_attr_pos)) {
185
0
        return true;
186
0
    }
187
188
0
    mark_attr_moved(new_xml, old_attr, new_attr, old_pos, new_pos);
189
0
    return true;
190
0
}
191
192
/*!
193
 * \internal
194
 * \brief Mark a new attribute dirty if ACLs allow creation, or remove otherwise
195
 *
196
 * We set the \c pcmk__xf_created flag on all attributes in the new XML at an
197
 * earlier stage of change calculation. Then we checked whether each attribute
198
 * was present in the old XML, and we cleared the flag if so. If the flag is
199
 * still set, then the attribute is truly new.
200
 *
201
 * Now we check whether ACLs allow the attribute's creation. If so, we "accept"
202
 * it: we mark the attribute as dirty and modified, and we mark all of its
203
 * parents as dirty. Otherwise, we reject it by removing the attribute (ignoring
204
 * ACLs and change tracking for the removal).
205
 *
206
 * \param[in,out] attr       XML attribute to mark dirty or remove
207
 * \param[in]     user_data  Ignored
208
 *
209
 * \return \c true (to continue iterating)
210
 *
211
 * \note This is compatible with \c pcmk__xe_foreach_attr().
212
 */
213
static bool
214
check_new_attr_acls(xmlAttr *attr, void *user_data)
215
0
{
216
0
    const char *name = (const char *) attr->name;
217
0
    const char *value = pcmk__xml_attr_value(attr);
218
0
    const xml_node_private_t *nodepriv = attr->_private;
219
0
    xmlNode *new_xml = attr->parent;
220
0
    const char *new_xml_id = pcmk__s(pcmk__xe_id(new_xml), "without ID");
221
222
0
    if (!pcmk__is_set(nodepriv->flags, pcmk__xf_created)) {
223
0
        return true;
224
0
    }
225
226
    /* Check ACLs (we can't use the remove-then-create trick because it
227
     * would modify the attribute position).
228
     */
229
0
    if (!pcmk__check_acl(new_xml, name, pcmk__xf_acl_write)) {
230
0
        pcmk__trace("ACLs prevent creation of attribute %s=%s in %s %s", name,
231
0
                    value, (const char *) new_xml->name, new_xml_id);
232
0
        pcmk__xa_remove(attr, true);
233
0
        return true;
234
0
    }
235
236
0
    pcmk__trace("Created new attribute %s=%s in %s %s", name, value,
237
0
                (const char *) new_xml->name, new_xml_id);
238
0
    pcmk__mark_xml_attr_dirty(attr);
239
0
    return true;
240
0
}
241
242
/*!
243
 * \internal
244
 * \brief Calculate differences in attributes between two XML nodes
245
 *
246
 * \param[in,out] old_xml  Original XML to compare
247
 * \param[in,out] new_xml  New XML to compare
248
 */
249
static void
250
xml_diff_attrs(xmlNode *old_xml, xmlNode *new_xml)
251
0
{
252
    // Cleared later if attributes are not really new
253
0
    pcmk__xe_foreach_attr(new_xml, mark_attr_created, NULL);
254
255
0
    pcmk__xe_foreach_attr(old_xml, mark_attr_diff, new_xml);
256
0
    pcmk__xe_foreach_attr(new_xml, check_new_attr_acls, NULL);
257
0
}
258
259
/*!
260
 * \internal
261
 * \brief Add a deleted object record for an old XML child if ACLs allow
262
 *
263
 * This is intended to be called for a child of an old XML element that is not
264
 * present as a child of a new XML element.
265
 *
266
 * Add a temporary copy of the old child to the new XML. Then check whether ACLs
267
 * would have allowed the deletion of that element. If so, add a deleted object
268
 * record for it to the new XML's document, and set the \c pcmk__xf_skip flag on
269
 * the old child.
270
 *
271
 * The temporary copy is removed before returning. The new XML and all of its
272
 * ancestors will have the \c pcmk__xf_dirty flag set because of the creation,
273
 * however.
274
 *
275
 * \param[in,out] old_child   Child of old XML
276
 * \param[in,out] new_parent  New XML that does not contain \p old_child
277
 *
278
 * \note The deletion is checked using the new XML's ACLs. The ACLs may have
279
 *       also changed between the old and new XML trees. Callers should take
280
 *       reasonable action if there were ACL changes that themselves would have
281
 *       been denied.
282
 */
283
static void
284
mark_child_deleted(xmlNode *old_child, xmlNode *new_parent)
285
0
{
286
0
    int pos = pcmk__xml_position(old_child, pcmk__xf_skip);
287
288
    // Re-create the child element so we can check ACLs
289
0
    xmlNode *candidate = pcmk__xml_copy(new_parent, old_child);
290
291
    // Clear flags on new child and its children
292
0
    pcmk__xml_tree_foreach(candidate, pcmk__xml_reset_node_flags, NULL);
293
294
    // pcmk__xml_free_position() will check whether ACLs allow the deletion
295
0
    pcmk__apply_acls(candidate->doc);
296
297
    /* Try to remove the child again (which will track it in document's
298
     * deleted_objs on success)
299
     */
300
0
    if (pcmk__xml_free_position(candidate, pos) != pcmk_rc_ok) {
301
        // ACLs denied deletion in pcmk__xml_free_position(), so free here
302
0
        pcmk__xml_free_node(candidate);
303
0
    }
304
305
0
    pcmk__set_xml_flags((xml_node_private_t *) old_child->_private,
306
0
                        pcmk__xf_skip);
307
0
}
308
309
/*!
310
 * \internal
311
 * \brief Mark a new child as moved and set \c pcmk__xf_skip as appropriate
312
 *
313
 * \param[in,out] old_child  Child of old XML
314
 * \param[in,out] new_child  Child of new XML that matches \p old_child
315
 * \param[in]     old_pos    Position of \p old_child among its siblings
316
 * \param[in]     new_pos    Position of \p new_child among its siblings
317
 */
318
static void
319
mark_child_moved(xmlNode *old_child, xmlNode *new_child, int old_pos,
320
                 int new_pos)
321
0
{
322
0
    const char *id_s = pcmk__s(pcmk__xe_id(new_child), "<no id>");
323
0
    xmlNode *new_parent = new_child->parent;
324
0
    xml_node_private_t *nodepriv = new_child->_private;
325
326
0
    pcmk__trace("Child element %s with " PCMK_XA_ID "='%s' moved from position "
327
0
                "%d to %d under %s",
328
0
                new_child->name, id_s, old_pos, new_pos, new_parent->name);
329
0
    pcmk__mark_xml_node_dirty(new_parent);
330
0
    pcmk__set_xml_flags(nodepriv, pcmk__xf_moved);
331
332
    /* @TODO Figure out and document why we skip the old child in future
333
     * position calculations if the old position is higher, and skip the new
334
     * child in future position calculations if the new position is higher. This
335
     * goes back to d028b52, and there's no explanation in the commit message.
336
     */
337
0
    if (old_pos > new_pos) {
338
0
        nodepriv = old_child->_private;
339
0
    }
340
0
    pcmk__set_xml_flags(nodepriv, pcmk__xf_skip);
341
0
}
342
343
/*!
344
 * \internal
345
 * \brief Set the \c pcmk__xf_dirty and \c pcmk__xf_created flags on an XML node
346
 *
347
 * \param[in,out] xml        Node whose flags to set
348
 * \param[in]     user_data  Ignored
349
 *
350
 * \return \c true (to continue traversing the tree)
351
 *
352
 * \note This is compatible with \c pcmk__xml_tree_foreach().
353
 */
354
static bool
355
mark_xml_dirty_created(xmlNode *xml, void *user_data)
356
0
{
357
0
    xml_node_private_t *nodepriv = xml->_private;
358
359
0
    if (nodepriv != NULL) {
360
0
        pcmk__set_xml_flags(nodepriv, pcmk__xf_dirty|pcmk__xf_created);
361
0
    }
362
0
    return true;
363
0
}
364
365
/*!
366
 * \internal
367
 * \brief Mark a child as created, and free it if ACLs disallow its creation
368
 *
369
 * This sets the \c pcmk__xf_skip, \c pcmk__xf_dirty, and \c pcmk__xf_created
370
 * flags on \p new_child, and it sets dirty flags on all ancestor nodes and the
371
 * document.
372
 *
373
 * \param[in,out] new_child  Newly created child of new XML node
374
 */
375
static void
376
mark_child_created(xmlNode *new_child)
377
0
{
378
0
    xml_node_private_t *nodepriv = new_child->_private;
379
380
    /* Setting all these flags first seems like wasted work (albeit not much) if
381
     * pcmk__check_creation_acls() ends up freeing new_child. It also sets dirty
382
     * flags on the ancestors and document even if new_child ends up getting
383
     * freed. We do these steps first because:
384
     * - Currently pcmk__check_creation_acls() does nothing for nodes that don't
385
     *   have the pcmk__xf_created flag set.
386
     * - Otherwise we have a use-after-free if new_child gets freed.
387
     *
388
     * @TODO Create a way to call pcmk__check_creation_acls() first.
389
     */
390
391
    // @TODO Why do we set pcmk__xf_skip here?
392
0
    pcmk__set_xml_flags(nodepriv, pcmk__xf_skip);
393
394
    // Mark all ancestors and document dirty
395
0
    pcmk__mark_xml_node_dirty(new_child);
396
397
    // Mark new_child and all descendants dirty and created
398
0
    pcmk__xml_tree_foreach(new_child, mark_xml_dirty_created, NULL);
399
400
    // Check whether creation was allowed (may free new_child)
401
0
    pcmk__check_creation_acls(new_child);
402
0
}
403
404
/*!
405
 * \internal
406
 * \brief Check whether a new XML child comment matches an old XML child comment
407
 *
408
 * Two comments match if they have the same position among their siblings and
409
 * the same contents.
410
 *
411
 * If \p new_comment has the \c pcmk__xf_skip flag set, then it is automatically
412
 * considered not to match.
413
 *
414
 * \param[in] old_comment  Old XML child element
415
 * \param[in] new_comment  New XML child element
416
 *
417
 * \retval \c true   if \p new_comment matches \p old_comment
418
 * \retval \c false  otherwise
419
 */
420
static bool
421
new_comment_matches(const xmlNode *old_comment, const xmlNode *new_comment)
422
0
{
423
0
    xml_node_private_t *nodepriv = new_comment->_private;
424
425
0
    if (pcmk__is_set(nodepriv->flags, pcmk__xf_skip)) {
426
        /* @TODO Should we also return false if old_comment has pcmk__xf_skip
427
         * set? This preserves existing behavior at time of writing.
428
         */
429
0
        return false;
430
0
    }
431
0
    if (pcmk__xml_position(old_comment, pcmk__xf_skip)
432
0
        != pcmk__xml_position(new_comment, pcmk__xf_skip)) {
433
0
        return false;
434
0
    }
435
0
    return pcmk__xc_matches(old_comment, new_comment);
436
0
}
437
438
/*!
439
 * \internal
440
 * \brief Check whether a new XML child element matches an old XML child element
441
 *
442
 * Two elements match if they have the same name and the same ID. (Both IDs can
443
 * be \c NULL.)
444
 *
445
 * For XML attributes other than \c PCMK_XA_ID, we can treat a value change as
446
 * an in-place modification. However, when Pacemaker applies a patchset, it uses
447
 * the \c PCMK_XA_ID attribute to find the node to update (modify, delete, or
448
 * move). If we treat two nodes with different \c PCMK_XA_ID attributes as
449
 * matching and then mark that attribute as changed, it can cause this lookup to
450
 * fail.
451
 *
452
 * There's unlikely to ever be much practical reason to treat elements with
453
 * different IDs as a change. Unless that changes, we'll treat them as a
454
 * mismatch.
455
 *
456
 * \param[in] old_element  Old XML child element
457
 * \param[in] new_element  New XML child element
458
 *
459
 * \retval \c true   if \p new_element matches \p old_element
460
 * \retval \c false  otherwise
461
 */
462
static bool
463
new_element_matches(const xmlNode *old_element, const xmlNode *new_element)
464
0
{
465
0
    return pcmk__xe_is(new_element, (const char *) old_element->name)
466
0
           && pcmk__str_eq(pcmk__xe_id(old_element), pcmk__xe_id(new_element),
467
0
                           pcmk__str_none);
468
0
}
469
470
/*!
471
 * \internal
472
 * \brief Check whether a new XML child node matches an old XML child node
473
 *
474
 * Node types must be the same in order to match.
475
 *
476
 * For comments, a match is a comment at the same position with the same
477
 * content.
478
 *
479
 * For elements, a match is an element with the same name and the same ID. (Both
480
 * IDs can be \c NULL.)
481
 *
482
 * For other node types, there is no match.
483
 *
484
 * \param[in] old_child  Child of old XML
485
 * \param[in] new_child  Child of new XML
486
 *
487
 * \retval \c true   if \p new_child matches \p old_child
488
 * \retval \c false  otherwise
489
 */
490
static bool
491
new_child_matches(const xmlNode *old_child, const xmlNode *new_child)
492
0
{
493
0
    if (old_child->type != new_child->type) {
494
0
        return false;
495
0
    }
496
497
0
    switch (old_child->type) {
498
0
        case XML_COMMENT_NODE:
499
0
            return new_comment_matches(old_child, new_child);
500
0
        case XML_ELEMENT_NODE:
501
0
            return new_element_matches(old_child, new_child);
502
0
        default:
503
0
            return false;
504
0
    }
505
0
}
506
507
/*!
508
 * \internal
509
 * \brief Set old and new child's \c match pointers to each other if they match
510
 *
511
 * A node that is part of a matching pair gets its <tt>_private:match</tt>
512
 * member set to the matching node.
513
 *
514
 * \param[in,out] new_child  New child
515
 * \param[in,out] user_data  Old child (<tt>xmlNode *</tt>)
516
 *
517
 * \return \c true (to continue iterating over new children) if the nodes don't
518
 *         match, or \c false (to stop iterating) if they do
519
 */
520
static bool
521
set_match_if_matching(xmlNode *new_child, void *user_data)
522
0
{
523
0
    xmlNode *old_child = user_data;
524
0
    xml_node_private_t *old_nodepriv = old_child->_private;
525
0
    xml_node_private_t *new_nodepriv = new_child->_private;
526
527
0
    if ((new_nodepriv == NULL) || (new_nodepriv->match != NULL)) {
528
        // Can't process, or this new child already matched some old child
529
0
        return true;
530
0
    }
531
532
0
    if (!new_child_matches(old_child, new_child)) {
533
0
        return true;
534
0
    }
535
536
0
    old_nodepriv->match = new_child;
537
0
    new_nodepriv->match = old_child;
538
0
    return false;
539
0
}
540
541
/*!
542
 * \internal
543
 * \brief Find a child of a new XML node that matches a child of an old node
544
 *
545
 * If a match is found, set the <tt>_private:child</tt> pointers in the matching
546
 * old and new children to each other.
547
 *
548
 * \param[in,out] old_child  Child of old XML node
549
 * \param[in,out] user_data  New XML node (<tt>xmlNode *</tt>)
550
 *
551
 * \return \c true (to continue iterating over old children)
552
 */
553
static bool
554
find_and_set_match(xmlNode *old_child, void *user_data)
555
0
{
556
0
    xmlNode *new_xml = user_data;
557
0
    xml_node_private_t *old_nodepriv = old_child->_private;
558
559
0
    if ((old_nodepriv == NULL) || (old_nodepriv->match != NULL)) {
560
        // Can't process, or we already found a match for this old child
561
0
        return true;
562
0
    }
563
564
0
    pcmk__xml_foreach_child(new_xml, set_match_if_matching, old_child);
565
0
    return true;
566
0
}
567
568
/*!
569
 * \internal
570
 * \brief Mark a child node as changed or deleted if appropriate
571
 *
572
 * If the old child has its \c match pointer set, then it's present in the new
573
 * XML. It may or may not have changed. We make a recursive call to
574
 * \c pcmk__xml_mark_changes() to mark any changes that may be present.
575
 *
576
 * Otherwise, the old child is absent from the new node, so we mark it as
577
 * deleted.
578
 *
579
 * \param[in,out] old_child  Child of old XML node
580
 * \param[in,out] user_data  New XML node (<tt>xmlNode *</tt>)
581
 *
582
 * \return \c true (to continue iterating over old children)
583
 */
584
static bool
585
mark_child_changed_or_deleted(xmlNode *old_child, void *user_data)
586
0
{
587
0
    xmlNode *new_xml = user_data;
588
0
    xmlNode *new_child = NULL;
589
0
    xml_node_private_t *nodepriv = old_child->_private;
590
591
0
    if (nodepriv == NULL) {
592
0
        return true;
593
0
    }
594
595
0
    if (nodepriv->match == NULL) {
596
        // No match in new XML means the old child was deleted
597
0
        mark_child_deleted(old_child, new_xml);
598
0
        return true;
599
0
    }
600
601
    /* Fetch the match and clear old_child->_private's match member.
602
     * new_child->_private's match member is handled in
603
     * mark_child_moved_or_created().
604
     */
605
0
    new_child = nodepriv->match;
606
0
    nodepriv->match = NULL;
607
608
0
    pcmk__assert(old_child->type == new_child->type);
609
610
0
    if (old_child->type == XML_COMMENT_NODE) {
611
        // Comments match only if their positions and contents match
612
0
        return true;
613
0
    }
614
615
0
    pcmk__xml_mark_changes(old_child, new_child);
616
0
    return true;
617
0
}
618
619
/*!
620
 * \internal
621
 * \brief Mark a child node as moved or created if appropriate
622
 *
623
 * If the new child has its \c match pointer set, then it's present in the old
624
 * XML. Any changes within the child were marked in
625
 * \c mark_child_changed_or_moved(). It may or may not have moved. We check for
626
 * that and mark the move here if so.
627
 *
628
 * Otherwise, the new child is absent from the old node, so we mark it as
629
 * created.
630
 *
631
 * \param[in,out] new_child  Child of new XML node
632
 * \param[in]     user_data  Ignored
633
 *
634
 * \return \c true (to continue iterating over new children)
635
 *
636
 * \note This frees \p new_child if it's newly created and ACLs disallow the
637
 *       creation.
638
 */
639
static bool
640
mark_child_moved_or_created(xmlNode *new_child, void *user_data)
641
0
{
642
0
    xmlNode *old_child = NULL;
643
0
    int old_pos = 0;
644
0
    int new_pos = 0;
645
0
    xml_node_private_t *nodepriv = new_child->_private;
646
647
0
    if (nodepriv == NULL) {
648
0
        return true;
649
0
    }
650
651
0
    if (nodepriv->match == NULL) {
652
        // No match in old XML means the new child is newly created
653
0
        mark_child_created(new_child);
654
0
        return true;
655
0
    }
656
657
    /* Fetch the match and clear new_child->_private's match member. Any changes
658
     * within the child were marked by mark_child_changed_or_deleted(). If the
659
     * child was moved, mark the move now.
660
     *
661
     * We might be able to mark the move in mark_child_changed_or_deleted(),
662
     * consolidating both actions. We'd have to think about whether the timing
663
     * of setting the pcmk__xf_skip flag makes any difference.
664
     */
665
0
    old_child = nodepriv->match;
666
0
    nodepriv->match = NULL;
667
668
0
    old_pos = pcmk__xml_position(old_child, pcmk__xf_skip);
669
0
    new_pos = pcmk__xml_position(new_child, pcmk__xf_skip);
670
671
0
    if (old_pos != new_pos) {
672
0
        mark_child_moved(old_child, new_child, old_pos, new_pos);
673
0
    }
674
675
0
    return true;
676
0
}
677
678
/*!
679
 * \internal
680
 * \brief Mark changes between two XML trees
681
 *
682
 * Set flags in a new XML tree to indicate changes relative to an old XML tree.
683
 *
684
 * \param[in,out] old_xml  XML before changes
685
 * \param[in,out] new_xml  XML after changes
686
 *
687
 * \note This may set \c pcmk__xf_skip on parts of \p old_xml.
688
 * \note This function is recursive via \c mark_child_changed_or_deleted().
689
 */
690
void
691
pcmk__xml_mark_changes(xmlNode *old_xml, xmlNode *new_xml)
692
0
{
693
    /* This function may set the xml_node_private_t:match member on children of
694
     * old_xml and new_xml, but it clears that member before returning.
695
     *
696
     * @TODO Ensure we handle (for example, by copying) or reject user-created
697
     * XML that is missing xml_node_private_t at top level or in any children.
698
     * Similarly, check handling of node types for which we don't create private
699
     * data. For now, we'll skip them in the loops below.
700
     */
701
0
    CRM_CHECK((old_xml != NULL) && (new_xml != NULL), return);
702
0
    if ((old_xml->_private == NULL) || (new_xml->_private == NULL)) {
703
0
        return;
704
0
    }
705
706
0
    pcmk__xml_doc_set_flags(new_xml->doc, pcmk__xf_tracking);
707
0
    xml_diff_attrs(old_xml, new_xml);
708
709
0
    pcmk__xml_foreach_child(old_xml, find_and_set_match, new_xml);
710
0
    pcmk__xml_foreach_child(old_xml, mark_child_changed_or_deleted, new_xml);
711
0
    pcmk__xml_foreach_child(new_xml, mark_child_moved_or_created, NULL);
712
0
}
713
714
/*!
715
 * \internal
716
 * \brief Check whether an attribute is marked as deleted
717
 *
718
 * \param[in] attr       XML attribute
719
 * \param[in] user_data  Ignored
720
 *
721
 * \return \c true if \c pcmk__xf_deleted is set for \p attr, or \c false
722
 *         otherwise
723
 *
724
 * \note This is compatible with \c pcmk__xe_remove_matching_attrs().
725
 */
726
static bool
727
marked_as_deleted(const xmlAttr *attr, void *user_data)
728
0
{
729
0
    const xml_node_private_t *nodepriv = attr->_private;
730
731
0
    return pcmk__is_set(nodepriv->flags, pcmk__xf_deleted);
732
0
}
733
734
/*!
735
 * \internal
736
 * \brief Clear flags on an XML attribute
737
 *
738
 * \param[in,out] xml        XML attribute whose flags to reset
739
 * \param[in,out] user_data  Ignored
740
 *
741
 * \return \c true (to continue iterating)
742
 *
743
 * \note This is compatible with \c pcmk__xe_foreach_attr().
744
 */
745
static bool
746
reset_attr_flags(xmlAttr *attr, void *user_data)
747
0
{
748
0
    pcmk__xml_reset_node_flags((xmlNode *) attr, user_data);
749
0
    return true;
750
0
}
751
752
/*!
753
 * \internal
754
 * \brief Commit an XML node's attribute deletions
755
 *
756
 * This clears the node's flags, deletes any of its attributes that have the
757
 * \c pcmk__xf_deleted flag set, and clears the remaining attributes' flags.
758
 *
759
 * \param[in,out] xml        XML node whose attribute deletions to commit
760
 * \param[in,out] user_data  Ignored
761
 *
762
 * \return \c true (to continue traversing the tree)
763
 *
764
 * \note This is compatible with \c pcmk__xml_tree_foreach().
765
 */
766
static bool
767
commit_attr_deletions(xmlNode *xml, void *user_data)
768
0
{
769
0
    pcmk__xml_reset_node_flags(xml, NULL);
770
0
    pcmk__xe_remove_matching_attrs(xml, true, marked_as_deleted, NULL);
771
0
    pcmk__xe_foreach_attr(xml, reset_attr_flags, NULL);
772
0
    return true;
773
0
}
774
775
/*!
776
 * \internal
777
 * \brief Finalize all pending changes to an XML document and reset private data
778
 *
779
 * Clear the ACL user and all flags, unpacked ACLs, and deleted node records for
780
 * the document; clear all flags on each node in the tree; and delete any
781
 * attributes that are marked for deletion.
782
 *
783
 * \param[in,out] doc  XML document
784
 *
785
 * \note When change tracking is enabled, "deleting" an attribute simply marks
786
 *       it for deletion (using \c pcmk__xf_deleted) until changes are
787
 *       committed. Freeing a node (using \c pcmk__xml_free()) adds a deleted
788
 *       node record (\c pcmk__deleted_xml_t) to the node's document before
789
 *       freeing it.
790
 * \note This function clears all flags, not just flags that indicate changes.
791
 *       In particular, note that it clears the \c pcmk__xf_tracking flag, thus
792
 *       disabling tracking.
793
 */
794
void
795
pcmk__xml_commit_changes(xmlDoc *doc)
796
0
{
797
0
    xml_doc_private_t *docpriv = NULL;
798
799
0
    if (doc == NULL) {
800
0
        return;
801
0
    }
802
803
0
    docpriv = doc->_private;
804
0
    if (docpriv == NULL) {
805
0
        return;
806
0
    }
807
808
0
    if (pcmk__is_set(docpriv->flags, pcmk__xf_dirty)) {
809
0
        pcmk__xml_tree_foreach(xmlDocGetRootElement(doc), commit_attr_deletions,
810
                               NULL);
811
0
    }
812
0
    pcmk__xml_reset_doc_private_data(docpriv);
813
0
}