root/lib/common/xml.c

/* [previous][next][first][last][top][bottom][index][help] */

DEFINITIONS

This source file includes following definitions.
  1. pcmk__xml_element_type_text
  2. pcmk__xml_tree_foreach
  3. pcmk__xml_set_parent_flags
  4. pcmk__xml_doc_set_flags
  5. pcmk__xml_doc_all_flags_set
  6. pcmk__mark_xml_node_dirty
  7. pcmk__xml_reset_node_flags
  8. mark_xml_dirty_created
  9. mark_xml_tree_dirty_created
  10. free_deleted_object
  11. reset_xml_private_data
  12. new_private_data
  13. free_private_data
  14. pcmk__xml_new_private_data
  15. pcmk__xml_free_private_data
  16. pcmk__xml_position
  17. commit_attr_deletions
  18. pcmk__xml_commit_changes
  19. pcmk__xml_new_doc
  20. pcmk__xml_free_doc
  21. pcmk__xml_is_name_start_char
  22. pcmk__xml_is_name_char
  23. pcmk__xml_sanitize_id
  24. pcmk__xml_free_node
  25. free_xml_with_position
  26. pcmk__xml_free
  27. pcmk__xml_copy
  28. pcmk__strip_xml_text
  29. pcmk__xml_needs_escape
  30. pcmk__xml_escape
  31. mark_attr_deleted
  32. mark_attr_changed
  33. mark_attr_moved
  34. xml_diff_old_attrs
  35. mark_created_attrs
  36. xml_diff_attrs
  37. mark_child_deleted
  38. mark_child_moved
  39. new_comment_matches
  40. new_element_matches
  41. new_child_matches
  42. find_matching_children
  43. pcmk__xml_mark_changes
  44. pcmk__xml_artefact_root
  45. find_artefact
  46. pcmk__xml_artefact_path
  47. copy_xml
  48. crm_xml_init
  49. crm_xml_cleanup
  50. pcmk_free_xml_subtree
  51. free_xml
  52. crm_xml_sanitize_id
  53. xml_tracking_changes
  54. xml_document_dirty
  55. xml_accept_changes
  56. xml_track_changes
  57. xml_calculate_changes
  58. xml_calculate_significant_changes

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

/* [previous][next][first][last][top][bottom][index][help] */