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