diff options
Diffstat (limited to '')
-rw-r--r-- | lib/common/xml.c | 2825 |
1 files changed, 1407 insertions, 1418 deletions
diff --git a/lib/common/xml.c b/lib/common/xml.c index 53ebff7..73d6025 100644 --- a/lib/common/xml.c +++ b/lib/common/xml.c @@ -1,5 +1,5 @@ /* - * Copyright 2004-2023 the Pacemaker project contributors + * Copyright 2004-2024 the Pacemaker project contributors * * The version control history for this file may have further details. * @@ -9,41 +9,52 @@ #include <crm_internal.h> +#include <stdarg.h> +#include <stdint.h> // uint32_t #include <stdio.h> -#include <sys/types.h> -#include <unistd.h> -#include <time.h> -#include <string.h> #include <stdlib.h> -#include <stdarg.h> -#include <bzlib.h> +#include <string.h> +#include <sys/stat.h> // stat(), S_ISREG, etc. +#include <sys/types.h> #include <libxml/parser.h> #include <libxml/tree.h> -#include <libxml/xmlIO.h> /* xmlAllocOutputBuffer */ #include <crm/crm.h> -#include <crm/msg_xml.h> #include <crm/common/xml.h> -#include <crm/common/xml_internal.h> // PCMK__XML_LOG_BASE, etc. +#include <crm/common/xml_internal.h> // PCMK__XML_LOG_BASE, etc. #include "crmcommon_private.h" -// Define this as 1 in development to get insanely verbose trace messages -#ifndef XML_PARSER_DEBUG -#define XML_PARSER_DEBUG 0 -#endif - -/* @TODO XML_PARSE_RECOVER allows some XML errors to be silently worked around - * by libxml2, which is potentially ambiguous and dangerous. We should drop it - * when we can break backward compatibility with configurations that might be - * relying on it (i.e. pacemaker 3.0.0). +/*! + * \internal + * \brief Apply a function to each XML node in a tree (pre-order, depth-first) * - * It might be a good idea to have a transitional period where we first try - * parsing without XML_PARSE_RECOVER, and if that fails, try parsing again with - * it, logging a warning if it succeeds. + * \param[in,out] xml XML tree to traverse + * \param[in,out] fn Function to call for each node (returns \c true to + * continue traversing the tree or \c false to stop) + * \param[in,out] user_data Argument to \p fn + * + * \return \c false if any \p fn call returned \c false, or \c true otherwise + * + * \note This function is recursive. */ -#define PCMK__XML_PARSE_OPTS_WITHOUT_RECOVER (XML_PARSE_NOBLANKS) -#define PCMK__XML_PARSE_OPTS_WITH_RECOVER (XML_PARSE_NOBLANKS | XML_PARSE_RECOVER) +bool +pcmk__xml_tree_foreach(xmlNode *xml, bool (*fn)(xmlNode *, void *), + void *user_data) +{ + if (!fn(xml, user_data)) { + return false; + } + + for (xml = pcmk__xml_first_child(xml); xml != NULL; + xml = pcmk__xml_next(xml)) { + + if (!pcmk__xml_tree_foreach(xml, fn, user_data)) { + return false; + } + } + return true; +} bool pcmk__tracking_xml_changes(xmlNode *xml, bool lazy) @@ -93,43 +104,72 @@ pcmk__mark_xml_node_dirty(xmlNode *xml) set_parent_flag(xml, pcmk__xf_dirty); } -// Clear flags on XML node and its children -static void -reset_xml_node_flags(xmlNode *xml) +/*! + * \internal + * \brief Clear flags on an XML node + * + * \param[in,out] xml XML node whose flags to reset + * \param[in,out] user_data Ignored + * + * \return \c true (to continue traversing the tree) + * + * \note This is compatible with \c pcmk__xml_tree_foreach(). + */ +static bool +reset_xml_node_flags(xmlNode *xml, void *user_data) { - xmlNode *cIter = NULL; xml_node_private_t *nodepriv = xml->_private; - if (nodepriv) { - nodepriv->flags = 0; + if (nodepriv != NULL) { + nodepriv->flags = pcmk__xf_none; } + return true; +} - for (cIter = pcmk__xml_first_child(xml); cIter != NULL; - cIter = pcmk__xml_next(cIter)) { - reset_xml_node_flags(cIter); +/*! + * \internal + * \brief Set the \c pcmk__xf_dirty and \c pcmk__xf_created flags on an XML node + * + * \param[in,out] xml Node whose flags to set + * \param[in] user_data Ignored + * + * \return \c true (to continue traversing the tree) + * + * \note This is compatible with \c pcmk__xml_tree_foreach(). + */ +static bool +mark_xml_dirty_created(xmlNode *xml, void *user_data) +{ + xml_node_private_t *nodepriv = xml->_private; + + if (nodepriv != NULL) { + pcmk__set_xml_flags(nodepriv, pcmk__xf_dirty|pcmk__xf_created); } + return true; } -// Set xpf_created flag on XML node and any children +/*! + * \internal + * \brief Mark an XML tree as dirty and created, and mark its parents dirty + * + * Also mark the document dirty. + * + * \param[in,out] xml Tree to mark as dirty and created + */ void -pcmk__mark_xml_created(xmlNode *xml) +pcmk__xml_mark_created(xmlNode *xml) { - xmlNode *cIter = NULL; - xml_node_private_t *nodepriv = NULL; - CRM_ASSERT(xml != NULL); - nodepriv = xml->_private; - if (nodepriv && pcmk__tracking_xml_changes(xml, FALSE)) { - if (!pcmk_is_set(nodepriv->flags, pcmk__xf_created)) { - pcmk__set_xml_flags(nodepriv, pcmk__xf_created); - pcmk__mark_xml_node_dirty(xml); - } - for (cIter = pcmk__xml_first_child(xml); cIter != NULL; - cIter = pcmk__xml_next(cIter)) { - pcmk__mark_xml_created(cIter); - } + if (!pcmk__tracking_xml_changes(xml, false)) { + // Tracking is disabled for entire document + return; } + + // Mark all parents and document dirty + pcmk__mark_xml_node_dirty(xml); + + pcmk__xml_tree_foreach(xml, mark_xml_dirty_created, NULL); } #define XML_DOC_PRIVATE_MAGIC 0x81726354UL @@ -142,7 +182,7 @@ free_deleted_object(void *data) if(data) { pcmk__deleted_xml_t *deleted_obj = data; - free(deleted_obj->path); + g_free(deleted_obj->path); free(deleted_obj); } } @@ -220,9 +260,9 @@ new_private_data(xmlNode *node) { switch (node->type) { case XML_DOCUMENT_NODE: { - xml_doc_private_t *docpriv = NULL; - docpriv = calloc(1, sizeof(xml_doc_private_t)); - CRM_ASSERT(docpriv != NULL); + xml_doc_private_t *docpriv = + pcmk__assert_alloc(1, sizeof(xml_doc_private_t)); + docpriv->check = XML_DOC_PRIVATE_MAGIC; /* Flags will be reset if necessary when tracking is enabled */ pcmk__set_xml_flags(docpriv, pcmk__xf_dirty|pcmk__xf_created); @@ -232,9 +272,9 @@ new_private_data(xmlNode *node) case XML_ELEMENT_NODE: case XML_ATTRIBUTE_NODE: case XML_COMMENT_NODE: { - xml_node_private_t *nodepriv = NULL; - nodepriv = calloc(1, sizeof(xml_node_private_t)); - CRM_ASSERT(nodepriv != NULL); + xml_node_private_t *nodepriv = + pcmk__assert_alloc(1, sizeof(xml_node_private_t)); + nodepriv->check = XML_NODE_PRIVATE_MAGIC; /* Flags will be reset if necessary when tracking is enabled */ pcmk__set_xml_flags(nodepriv, pcmk__xf_dirty|pcmk__xf_created); @@ -314,21 +354,23 @@ pcmk__xml_position(const xmlNode *xml, enum xml_private_flags ignore_if_set) return position; } -// Remove all attributes marked as deleted from an XML node -static void -accept_attr_deletions(xmlNode *xml) +/*! + * \internal + * \brief Remove all attributes marked as deleted from an XML node + * + * \param[in,out] xml XML node whose deleted attributes to remove + * \param[in,out] user_data Ignored + * + * \return \c true (to continue traversing the tree) + * + * \note This is compatible with \c pcmk__xml_tree_foreach(). + */ +static bool +accept_attr_deletions(xmlNode *xml, void *user_data) { - // Clear XML node's flags - ((xml_node_private_t *) xml->_private)->flags = pcmk__xf_none; - - // Remove this XML node's attributes that were marked as deleted + reset_xml_node_flags(xml, NULL); pcmk__xe_remove_matching_attrs(xml, pcmk__marked_as_deleted, NULL); - - // Recursively do the same for this XML node's children - for (xmlNodePtr cIter = pcmk__xml_first_child(xml); cIter != NULL; - cIter = pcmk__xml_next(cIter)) { - accept_attr_deletions(cIter); - } + return true; } /*! @@ -348,10 +390,11 @@ pcmk__xml_match(const xmlNode *haystack, const xmlNode *needle, bool exact) return pcmk__xc_match(haystack, needle, exact); } else { - const char *id = ID(needle); - const char *attr = (id == NULL)? NULL : XML_ATTR_ID; + const char *id = pcmk__xe_id(needle); + const char *attr = (id == NULL)? NULL : PCMK_XA_ID; - return pcmk__xe_match(haystack, (const char *) needle->name, attr, id); + return pcmk__xe_first_child(haystack, (const char *) needle->name, attr, + id); } } @@ -377,207 +420,263 @@ xml_accept_changes(xmlNode * xml) } docpriv->flags = pcmk__xf_none; - accept_attr_deletions(top); -} - -xmlNode * -find_xml_node(const xmlNode *root, const char *search_path, gboolean must_find) -{ - xmlNode *a_child = NULL; - const char *name = (root == NULL)? "<NULL>" : (const char *) root->name; - - if (search_path == NULL) { - crm_warn("Will never find <NULL>"); - return NULL; - } - - for (a_child = pcmk__xml_first_child(root); a_child != NULL; - a_child = pcmk__xml_next(a_child)) { - if (strcmp((const char *)a_child->name, search_path) == 0) { - return a_child; - } - } - - if (must_find) { - crm_warn("Could not find %s in %s.", search_path, name); - } else if (root != NULL) { - crm_trace("Could not find %s in %s.", search_path, name); - } else { - crm_trace("Could not find %s in <NULL>.", search_path); - } - - return NULL; + pcmk__xml_tree_foreach(top, accept_attr_deletions, NULL); } -#define attr_matches(c, n, v) pcmk__str_eq(crm_element_value((c), (n)), \ - (v), pcmk__str_none) - /*! * \internal * \brief Find first XML child element matching given criteria * - * \param[in] parent XML element to search - * \param[in] node_name If not NULL, only match children of this type - * \param[in] attr_n If not NULL, only match children with an attribute + * \param[in] parent XML element to search (can be \c NULL) + * \param[in] node_name If not \c NULL, only match children of this type + * \param[in] attr_n If not \c NULL, only match children with an attribute * of this name. * \param[in] attr_v If \p attr_n and this are not NULL, only match children * with an attribute named \p attr_n and this value * - * \return Matching XML child element, or NULL if none found + * \return Matching XML child element, or \c NULL if none found */ xmlNode * -pcmk__xe_match(const xmlNode *parent, const char *node_name, - const char *attr_n, const char *attr_v) +pcmk__xe_first_child(const xmlNode *parent, const char *node_name, + const char *attr_n, const char *attr_v) { - CRM_CHECK(parent != NULL, return NULL); - CRM_CHECK(attr_v == NULL || attr_n != NULL, return NULL); + xmlNode *child = NULL; + const char *parent_name = "<null>"; - for (xmlNode *child = pcmk__xml_first_child(parent); child != NULL; - child = pcmk__xml_next(child)) { - if (pcmk__str_eq(node_name, (const char *) (child->name), - pcmk__str_null_matches) - && ((attr_n == NULL) || - (attr_v == NULL && xmlHasProp(child, (pcmkXmlStr) attr_n)) || - (attr_v != NULL && attr_matches(child, attr_n, attr_v)))) { - return child; + CRM_CHECK((attr_v == NULL) || (attr_n != NULL), return NULL); + + if (parent != NULL) { + child = parent->children; + while ((child != NULL) && (child->type != XML_ELEMENT_NODE)) { + child = child->next; } + + parent_name = (const char *) parent->name; } - crm_trace("XML child node <%s%s%s%s%s> not found in %s", - (node_name? node_name : "(any)"), - (attr_n? " " : ""), - (attr_n? attr_n : ""), - (attr_n? "=" : ""), - (attr_n? attr_v : ""), - (const char *) parent->name); - return NULL; -} -void -copy_in_properties(xmlNode *target, const xmlNode *src) -{ - if (src == NULL) { - crm_warn("No node to copy properties from"); + for (; child != NULL; child = pcmk__xe_next(child)) { + const char *value = NULL; - } else if (target == NULL) { - crm_err("No node to copy properties into"); + if ((node_name != NULL) && !pcmk__xe_is(child, node_name)) { + // Node name mismatch + continue; + } + if (attr_n == NULL) { + // No attribute match needed + return child; + } - } else { - for (xmlAttrPtr a = pcmk__xe_first_attr(src); a != NULL; a = a->next) { - const char *p_name = (const char *) a->name; - const char *p_value = pcmk__xml_attr_value(a); + value = crm_element_value(child, attr_n); - expand_plus_plus(target, p_name, p_value); - if (xml_acl_denied(target)) { - crm_trace("Cannot copy %s=%s to %s", p_name, p_value, target->name); - return; - } + if ((attr_v == NULL) && (value != NULL)) { + // attr_v == NULL: Attribute attr_n must be set (to any value) + return child; + } + if ((attr_v != NULL) && (pcmk__str_eq(value, attr_v, pcmk__str_none))) { + // attr_v != NULL: Attribute attr_n must be set to value attr_v + return child; } } - return; + if (node_name == NULL) { + node_name = "(any)"; // For logging + } + if (attr_n != NULL) { + crm_trace("XML child node <%s %s=%s> not found in %s", + node_name, attr_n, attr_v, parent_name); + } else { + crm_trace("XML child node <%s> not found in %s", + node_name, parent_name); + } + return NULL; } /*! - * \brief Parse integer assignment statements on this node and all its child - * nodes + * \internal + * \brief Set an XML attribute, expanding \c ++ and \c += where appropriate + * + * If \p target already has an attribute named \p name set to an integer value + * and \p value is an addition assignment expression on \p name, then expand + * \p value to an integer and set attribute \p name to the expanded value in + * \p target. + * + * Otherwise, set attribute \p name on \p target using the literal \p value. + * + * The original attribute value in \p target and the number in an assignment + * expression in \p value are parsed and added as scores (that is, their values + * are capped at \c INFINITY and \c -INFINITY). For more details, refer to + * \c char2score(). * - * \param[in,out] target Root XML node to be processed + * For example, suppose \p target has an attribute named \c "X" with value + * \c "5", and that \p name is \c "X". + * * If \p value is \c "X++", the new value of \c "X" in \p target is \c "6". + * * If \p value is \c "X+=3", the new value of \c "X" in \p target is \c "8". + * * If \p value is \c "val", the new value of \c "X" in \p target is \c "val". + * * If \p value is \c "Y++", the new value of \c "X" in \p target is \c "Y++". * - * \note This function is recursive + * \param[in,out] target XML node whose attribute to set + * \param[in] name Name of the attribute to set + * \param[in] value New value of attribute to set + * + * \return Standard Pacemaker return code (specifically, \c EINVAL on invalid + * argument, or \c pcmk_rc_ok otherwise) */ -void -fix_plus_plus_recursive(xmlNode *target) +int +pcmk__xe_set_score(xmlNode *target, const char *name, const char *value) { - /* TODO: Remove recursion and use xpath searches for value++ */ - xmlNode *child = NULL; + const char *old_value = NULL; - for (xmlAttrPtr a = pcmk__xe_first_attr(target); a != NULL; a = a->next) { - const char *p_name = (const char *) a->name; - const char *p_value = pcmk__xml_attr_value(a); + CRM_CHECK((target != NULL) && (name != NULL), return EINVAL); - expand_plus_plus(target, p_name, p_value); + if (value == NULL) { + return pcmk_rc_ok; } - for (child = pcmk__xml_first_child(target); child != NULL; - child = pcmk__xml_next(child)) { - fix_plus_plus_recursive(child); + + old_value = crm_element_value(target, name); + + // If no previous value, skip to default case and set the value unexpanded. + if (old_value != NULL) { + const char *n = name; + const char *v = value; + + // Stop at first character that differs between name and value + for (; (*n == *v) && (*n != '\0'); n++, v++); + + // If value begins with name followed by a "++" or "+=" + if ((*n == '\0') + && (*v++ == '+') + && ((*v == '+') || (*v == '='))) { + + // If we're expanding ourselves, no previous value was set; use 0 + int old_value_i = (old_value != value)? char2score(old_value) : 0; + + /* value="X++": new value of X is old_value + 1 + * value="X+=Y": new value of X is old_value + Y (for some number Y) + */ + int add = (*v == '+')? 1 : char2score(++v); + + crm_xml_add_int(target, name, pcmk__add_scores(old_value_i, add)); + return pcmk_rc_ok; + } + } + + // Default case: set the attribute unexpanded (with value treated literally) + if (old_value != value) { + crm_xml_add(target, name, value); } + return pcmk_rc_ok; } /*! - * \brief Update current XML attribute value per parsed integer assignment - statement - * - * \param[in,out] target an XML node, containing a XML attribute that is - * initialized to some numeric value, to be processed - * \param[in] name name of the XML attribute, e.g. X, whose value - * should be updated - * \param[in] value assignment statement, e.g. "X++" or - * "X+=5", to be applied to the initialized value. - * - * \note The original XML attribute value is treated as 0 if non-numeric and - * truncated to be an integer if decimal-point-containing. - * \note The final XML attribute value is truncated to not exceed 1000000. - * \note Undefined behavior if unexpected input. + * \internal + * \brief Copy XML attributes from a source element to a target element + * + * This is similar to \c xmlCopyPropList() except that attributes are marked + * as dirty for change tracking purposes. + * + * \param[in,out] target XML element to receive copied attributes from \p src + * \param[in] src XML element whose attributes to copy to \p target + * \param[in] flags Group of <tt>enum pcmk__xa_flags</tt> + * + * \return Standard Pacemaker return code */ -void -expand_plus_plus(xmlNode * target, const char *name, const char *value) +int +pcmk__xe_copy_attrs(xmlNode *target, const xmlNode *src, uint32_t flags) { - int offset = 1; - int name_len = 0; - int int_value = 0; - int value_len = 0; + CRM_CHECK((src != NULL) && (target != NULL), return EINVAL); - const char *old_value = NULL; + for (xmlAttr *attr = pcmk__xe_first_attr(src); attr != NULL; + attr = attr->next) { - if (target == NULL || value == NULL || name == NULL) { - return; - } + const char *name = (const char *) attr->name; + const char *value = pcmk__xml_attr_value(attr); - old_value = crm_element_value(target, name); - - if (old_value == NULL) { - /* if no previous value, set unexpanded */ - goto set_unexpanded; + if (pcmk_is_set(flags, pcmk__xaf_no_overwrite) + && (crm_element_value(target, name) != NULL)) { + continue; + } - } else if (strstr(value, name) != value) { - goto set_unexpanded; + if (pcmk_is_set(flags, pcmk__xaf_score_update)) { + pcmk__xe_set_score(target, name, value); + } else { + crm_xml_add(target, name, value); + } } - name_len = strlen(name); - value_len = strlen(value); - if (value_len < (name_len + 2) - || value[name_len] != '+' || (value[name_len + 1] != '+' && value[name_len + 1] != '=')) { - goto set_unexpanded; - } + return pcmk_rc_ok; +} - /* if we are expanding ourselves, - * then no previous value was set and leave int_value as 0 - */ - if (old_value != value) { - int_value = char2score(old_value); +/*! + * \internal + * \brief Remove an XML attribute from an element + * + * \param[in,out] element XML element that owns \p attr + * \param[in,out] attr XML attribute to remove from \p element + * + * \return Standard Pacemaker return code (\c EPERM if ACLs prevent removal of + * attributes from \p element, or \c pcmk_rc_ok otherwise) + */ +static int +remove_xe_attr(xmlNode *element, xmlAttr *attr) +{ + if (attr == NULL) { + return pcmk_rc_ok; } - if (value[name_len + 1] != '+') { - const char *offset_s = value + (name_len + 2); + if (!pcmk__check_acl(element, NULL, pcmk__xf_acl_write)) { + // ACLs apply to element, not to particular attributes + crm_trace("ACLs prevent removal of attributes from %s element", + (const char *) element->name); + return EPERM; + } - offset = char2score(offset_s); + if (pcmk__tracking_xml_changes(element, false)) { + // Leave in place (marked for removal) until after diff is calculated + set_parent_flag(element, pcmk__xf_dirty); + pcmk__set_xml_flags((xml_node_private_t *) attr->_private, + pcmk__xf_deleted); + } else { + xmlRemoveProp(attr); } - int_value += offset; + return pcmk_rc_ok; +} - if (int_value > INFINITY) { - int_value = (int)INFINITY; +/*! + * \internal + * \brief Remove a named attribute from an XML element + * + * \param[in,out] element XML element to remove an attribute from + * \param[in] name Name of attribute to remove + */ +void +pcmk__xe_remove_attr(xmlNode *element, const char *name) +{ + if (name != NULL) { + remove_xe_attr(element, xmlHasProp(element, (pcmkXmlStr) name)); } +} - crm_xml_add_int(target, name, int_value); - return; +/*! + * \internal + * \brief Remove a named attribute from an XML element + * + * This is a wrapper for \c pcmk__xe_remove_attr() for use with + * \c pcmk__xml_tree_foreach(). + * + * \param[in,out] xml XML element to remove an attribute from + * \param[in] user_data Name of attribute to remove + * + * \return \c true (to continue traversing the tree) + * + * \note This is compatible with \c pcmk__xml_tree_foreach(). + */ +bool +pcmk__xe_remove_attr_cb(xmlNode *xml, void *user_data) +{ + const char *name = user_data; - set_unexpanded: - if (old_value == value) { - /* the old value is already set, nothing to do */ - return; - } - crm_xml_add(target, name, value); - return; + pcmk__xe_remove_attr(xml, name); + return true; } /*! @@ -599,102 +698,92 @@ pcmk__xe_remove_matching_attrs(xmlNode *element, for (xmlAttrPtr a = pcmk__xe_first_attr(element); a != NULL; a = next) { next = a->next; // Grab now because attribute might get removed if ((match == NULL) || match(a, user_data)) { - if (!pcmk__check_acl(element, NULL, pcmk__xf_acl_write)) { - crm_trace("ACLs prevent removal of attributes (%s and " - "possibly others) from %s element", - (const char *) a->name, (const char *) element->name); - return; // ACLs apply to element, not particular attributes - } - - if (pcmk__tracking_xml_changes(element, false)) { - // Leave (marked for removal) until after diff is calculated - set_parent_flag(element, pcmk__xf_dirty); - pcmk__set_xml_flags((xml_node_private_t *) a->_private, - pcmk__xf_deleted); - } else { - xmlRemoveProp(a); + if (remove_xe_attr(element, a) != pcmk_rc_ok) { + return; } } } } +/*! + * \internal + * \brief Create a new XML element under a given parent + * + * \param[in,out] parent XML element that will be the new element's parent + * (\c NULL to create a new XML document with the new + * node as root) + * \param[in] name Name of new element + * + * \return Newly created XML element (guaranteed not to be \c NULL) + */ xmlNode * -add_node_copy(xmlNode * parent, xmlNode * src_node) -{ - xmlNode *child = NULL; - - CRM_CHECK((parent != NULL) && (src_node != NULL), return NULL); - - child = xmlDocCopyNode(src_node, parent->doc, 1); - if (child == NULL) { - return NULL; - } - xmlAddChild(parent, child); - pcmk__mark_xml_created(child); - return child; -} - -xmlNode * -create_xml_node(xmlNode * parent, const char *name) +pcmk__xe_create(xmlNode *parent, const char *name) { - xmlDoc *doc = NULL; xmlNode *node = NULL; - if (pcmk__str_empty(name)) { - CRM_CHECK(name != NULL && name[0] == 0, return NULL); - return NULL; - } + CRM_ASSERT(!pcmk__str_empty(name)); if (parent == NULL) { - doc = xmlNewDoc((pcmkXmlStr) "1.0"); - if (doc == NULL) { - return NULL; - } + xmlDoc *doc = xmlNewDoc(PCMK__XML_VERSION); + + pcmk__mem_assert(doc); node = xmlNewDocRawNode(doc, NULL, (pcmkXmlStr) name, NULL); - if (node == NULL) { - xmlFreeDoc(doc); - return NULL; - } + pcmk__mem_assert(node); + xmlDocSetRootElement(doc, node); } else { node = xmlNewChild(parent, NULL, (pcmkXmlStr) name, NULL); - if (node == NULL) { - return NULL; - } + pcmk__mem_assert(node); } - pcmk__mark_xml_created(node); + + pcmk__xml_mark_created(node); return node; } -xmlNode * -pcmk_create_xml_text_node(xmlNode * parent, const char *name, const char *content) +/*! + * \internal + * \brief Set a formatted string as an XML node's content + * + * \param[in,out] node Node whose content to set + * \param[in] format <tt>printf(3)</tt>-style format string + * \param[in] ... Arguments for \p format + * + * \note This function escapes special characters. \c xmlNodeSetContent() does + * not. + */ +G_GNUC_PRINTF(2, 3) +void +pcmk__xe_set_content(xmlNode *node, const char *format, ...) { - xmlNode *node = create_xml_node(parent, name); - if (node != NULL) { - xmlNodeSetContent(node, (pcmkXmlStr) content); - } + const char *content = NULL; + char *buf = NULL; - return node; -} + if (strchr(format, '%') == NULL) { + // Nothing to format + content = format; -xmlNode * -pcmk_create_html_node(xmlNode * parent, const char *element_name, const char *id, - const char *class_name, const char *text) -{ - xmlNode *node = pcmk_create_xml_text_node(parent, element_name, text); + } else { + va_list ap; - if (class_name != NULL) { - crm_xml_add(node, "class", class_name); - } + va_start(ap, format); - if (id != NULL) { - crm_xml_add(node, "id", id); - } + if (pcmk__str_eq(format, "%s", pcmk__str_none)) { + // No need to make a copy + content = va_arg(ap, const char *); - return node; + } else { + CRM_ASSERT(vasprintf(&buf, format, ap) >= 0); + content = buf; + } + va_end(ap); + } + + xmlNodeSetContent(node, (pcmkXmlStr) content); + free(buf); + } } /*! @@ -710,72 +799,67 @@ pcmk_free_xml_subtree(xmlNode *xml) } static void -free_xml_with_position(xmlNode * child, int position) +free_xml_with_position(xmlNode *child, int position) { - if (child != NULL) { - xmlNode *top = NULL; - xmlDoc *doc = child->doc; - xml_node_private_t *nodepriv = child->_private; - xml_doc_private_t *docpriv = NULL; - - if (doc != NULL) { - top = xmlDocGetRootElement(doc); - } + xmlDoc *doc = NULL; + xml_node_private_t *nodepriv = NULL; - if (doc != NULL && top == child) { - /* Free everything */ - xmlFreeDoc(doc); + if (child == NULL) { + return; + } + doc = child->doc; + nodepriv = child->_private; - } else if (pcmk__check_acl(child, NULL, pcmk__xf_acl_write) == FALSE) { - GString *xpath = NULL; + if ((doc != NULL) && (xmlDocGetRootElement(doc) == child)) { + // Free everything + xmlFreeDoc(doc); + return; + } - pcmk__if_tracing({}, return); - xpath = pcmk__element_xpath(child); - qb_log_from_external_source(__func__, __FILE__, - "Cannot remove %s %x", LOG_TRACE, - __LINE__, 0, (const char *) xpath->str, - nodepriv->flags); - g_string_free(xpath, TRUE); - return; + if (!pcmk__check_acl(child, NULL, pcmk__xf_acl_write)) { + GString *xpath = NULL; - } else { - if (doc && pcmk__tracking_xml_changes(child, FALSE) - && !pcmk_is_set(nodepriv->flags, pcmk__xf_created)) { - - GString *xpath = pcmk__element_xpath(child); + pcmk__if_tracing({}, return); + xpath = pcmk__element_xpath(child); + qb_log_from_external_source(__func__, __FILE__, + "Cannot remove %s %x", LOG_TRACE, + __LINE__, 0, xpath->str, nodepriv->flags); + g_string_free(xpath, TRUE); + return; + } - if (xpath != NULL) { - pcmk__deleted_xml_t *deleted_obj = NULL; + if ((doc != NULL) && pcmk__tracking_xml_changes(child, false) + && !pcmk_is_set(nodepriv->flags, pcmk__xf_created)) { - crm_trace("Deleting %s %p from %p", - (const char *) xpath->str, child, doc); + xml_doc_private_t *docpriv = doc->_private; + GString *xpath = pcmk__element_xpath(child); - deleted_obj = calloc(1, sizeof(pcmk__deleted_xml_t)); - deleted_obj->path = strdup((const char *) xpath->str); + if (xpath != NULL) { + pcmk__deleted_xml_t *deleted_obj = NULL; - CRM_ASSERT(deleted_obj->path != NULL); - g_string_free(xpath, TRUE); + crm_trace("Deleting %s %p from %p", xpath->str, child, doc); - deleted_obj->position = -1; - /* Record the "position" only for XML comments for now */ - if (child->type == XML_COMMENT_NODE) { - if (position >= 0) { - deleted_obj->position = position; + deleted_obj = pcmk__assert_alloc(1, sizeof(pcmk__deleted_xml_t)); + deleted_obj->path = g_string_free(xpath, FALSE); + deleted_obj->position = -1; - } else { - deleted_obj->position = pcmk__xml_position(child, - pcmk__xf_skip); - } - } + // Record the position only for XML comments for now + if (child->type == XML_COMMENT_NODE) { + if (position >= 0) { + deleted_obj->position = position; - docpriv = doc->_private; - docpriv->deleted_objs = g_list_append(docpriv->deleted_objs, deleted_obj); - pcmk__set_xml_doc_flag(child, pcmk__xf_dirty); + } else { + deleted_obj->position = pcmk__xml_position(child, + pcmk__xf_skip); } } - pcmk_free_xml_subtree(child); + + docpriv->deleted_objs = g_list_append(docpriv->deleted_objs, + deleted_obj); + pcmk__set_xml_doc_flag(child, pcmk__xf_dirty); } } + pcmk_free_xml_subtree(child); } @@ -785,171 +869,48 @@ free_xml(xmlNode * child) free_xml_with_position(child, -1); } +/*! + * \internal + * \brief Make a deep copy of an XML node under a given parent + * + * \param[in,out] parent XML element that will be the copy's parent (\c NULL + * to create a new XML document with the copy as root) + * \param[in] src XML node to copy + * + * \return Deep copy of \p src, or \c NULL if \p src is \c NULL + */ xmlNode * -copy_xml(xmlNode * src) -{ - xmlDoc *doc = xmlNewDoc((pcmkXmlStr) "1.0"); - xmlNode *copy = xmlDocCopyNode(src, doc, 1); - - CRM_ASSERT(copy != NULL); - xmlDocSetRootElement(doc, copy); - return copy; -} - -xmlNode * -string2xml(const char *input) -{ - xmlNode *xml = NULL; - xmlDocPtr output = NULL; - xmlParserCtxtPtr ctxt = NULL; - const xmlError *last_error = NULL; - - if (input == NULL) { - crm_err("Can't parse NULL input"); - return NULL; - } - - /* create a parser context */ - ctxt = xmlNewParserCtxt(); - CRM_CHECK(ctxt != NULL, return NULL); - - xmlCtxtResetLastError(ctxt); - xmlSetGenericErrorFunc(ctxt, pcmk__log_xmllib_err); - output = xmlCtxtReadDoc(ctxt, (pcmkXmlStr) input, NULL, NULL, - PCMK__XML_PARSE_OPTS_WITHOUT_RECOVER); - - if (output == NULL) { - output = xmlCtxtReadDoc(ctxt, (pcmkXmlStr) input, NULL, NULL, - PCMK__XML_PARSE_OPTS_WITH_RECOVER); - if (output) { - crm_warn("Successfully recovered from XML errors " - "(note: a future release will treat this as a fatal failure)"); - } - } - - if (output) { - xml = xmlDocGetRootElement(output); - } - last_error = xmlCtxtGetLastError(ctxt); - if (last_error && last_error->code != XML_ERR_OK) { - /* crm_abort(__FILE__,__func__,__LINE__, "last_error->code != XML_ERR_OK", TRUE, TRUE); */ - /* - * http://xmlsoft.org/html/libxml-xmlerror.html#xmlErrorLevel - * http://xmlsoft.org/html/libxml-xmlerror.html#xmlParserErrors - */ - crm_warn("Parsing failed (domain=%d, level=%d, code=%d): %s", - last_error->domain, last_error->level, last_error->code, last_error->message); - - if (last_error->code == XML_ERR_DOCUMENT_EMPTY) { - CRM_LOG_ASSERT("Cannot parse an empty string"); - - } else if (last_error->code != XML_ERR_DOCUMENT_END) { - crm_err("Couldn't%s parse %d chars: %s", xml ? " fully" : "", (int)strlen(input), - input); - if (xml != NULL) { - crm_log_xml_err(xml, "Partial"); - } - - } else { - int len = strlen(input); - int lpc = 0; - - while(lpc < len) { - crm_warn("Parse error[+%.3d]: %.80s", lpc, input+lpc); - lpc += 80; - } - - CRM_LOG_ASSERT("String parsing error"); - } - } - - xmlFreeParserCtxt(ctxt); - return xml; -} - -xmlNode * -stdin2xml(void) -{ - size_t data_length = 0; - size_t read_chars = 0; - - char *xml_buffer = NULL; - xmlNode *xml_obj = NULL; - - do { - xml_buffer = pcmk__realloc(xml_buffer, data_length + PCMK__BUFFER_SIZE); - read_chars = fread(xml_buffer + data_length, 1, PCMK__BUFFER_SIZE, - stdin); - data_length += read_chars; - } while (read_chars == PCMK__BUFFER_SIZE); - - if (data_length == 0) { - crm_warn("No XML supplied on stdin"); - free(xml_buffer); - return NULL; - } - - xml_buffer[data_length] = '\0'; - xml_obj = string2xml(xml_buffer); - free(xml_buffer); - - crm_log_xml_trace(xml_obj, "Created fragment"); - return xml_obj; -} - -static char * -decompress_file(const char *filename) +pcmk__xml_copy(xmlNode *parent, xmlNode *src) { - char *buffer = NULL; - int rc = 0; - size_t length = 0, read_len = 0; - BZFILE *bz_file = NULL; - FILE *input = fopen(filename, "r"); + xmlNode *copy = NULL; - if (input == NULL) { - crm_perror(LOG_ERR, "Could not open %s for reading", filename); + if (src == NULL) { return NULL; } - bz_file = BZ2_bzReadOpen(&rc, input, 0, 0, NULL, 0); - rc = pcmk__bzlib2rc(rc); - - if (rc != pcmk_rc_ok) { - crm_err("Could not prepare to read compressed %s: %s " - CRM_XS " rc=%d", filename, pcmk_rc_str(rc), rc); - BZ2_bzReadClose(&rc, bz_file); - fclose(input); - return NULL; - } + if (parent == NULL) { + xmlDoc *doc = NULL; - rc = BZ_OK; - // cppcheck seems not to understand the abort-logic in pcmk__realloc - // cppcheck-suppress memleak - while (rc == BZ_OK) { - buffer = pcmk__realloc(buffer, PCMK__BUFFER_SIZE + length + 1); - read_len = BZ2_bzRead(&rc, bz_file, buffer + length, PCMK__BUFFER_SIZE); + // The copy will be the root element of a new document + CRM_ASSERT(src->type == XML_ELEMENT_NODE); - crm_trace("Read %ld bytes from file: %d", (long)read_len, rc); + doc = xmlNewDoc(PCMK__XML_VERSION); + pcmk__mem_assert(doc); - if (rc == BZ_OK || rc == BZ_STREAM_END) { - length += read_len; - } - } + copy = xmlDocCopyNode(src, doc, 1); + pcmk__mem_assert(copy); - buffer[length] = '\0'; + xmlDocSetRootElement(doc, copy); - rc = pcmk__bzlib2rc(rc); + } else { + copy = xmlDocCopyNode(src, parent->doc, 1); + pcmk__mem_assert(copy); - if (rc != pcmk_rc_ok) { - crm_err("Could not read compressed %s: %s " CRM_XS " rc=%d", - filename, pcmk_rc_str(rc), rc); - free(buffer); - buffer = NULL; + xmlAddChild(parent, copy); } - BZ2_bzReadClose(&rc, bz_file); - fclose(input); - return buffer; + pcmk__xml_mark_created(copy); + return copy; } /*! @@ -986,97 +947,6 @@ pcmk__strip_xml_text(xmlNode *xml) } } -xmlNode * -filename2xml(const char *filename) -{ - xmlNode *xml = NULL; - xmlDocPtr output = NULL; - bool uncompressed = true; - xmlParserCtxtPtr ctxt = NULL; - const xmlError *last_error = NULL; - - /* create a parser context */ - ctxt = xmlNewParserCtxt(); - CRM_CHECK(ctxt != NULL, return NULL); - - xmlCtxtResetLastError(ctxt); - xmlSetGenericErrorFunc(ctxt, pcmk__log_xmllib_err); - - if (filename) { - uncompressed = !pcmk__ends_with_ext(filename, ".bz2"); - } - - if (pcmk__str_eq(filename, "-", pcmk__str_null_matches)) { - /* STDIN_FILENO == fileno(stdin) */ - output = xmlCtxtReadFd(ctxt, STDIN_FILENO, "unknown.xml", NULL, - PCMK__XML_PARSE_OPTS_WITHOUT_RECOVER); - - if (output == NULL) { - output = xmlCtxtReadFd(ctxt, STDIN_FILENO, "unknown.xml", NULL, - PCMK__XML_PARSE_OPTS_WITH_RECOVER); - if (output) { - crm_warn("Successfully recovered from XML errors " - "(note: a future release will treat this as a fatal failure)"); - } - } - - } else if (uncompressed) { - output = xmlCtxtReadFile(ctxt, filename, NULL, - PCMK__XML_PARSE_OPTS_WITHOUT_RECOVER); - - if (output == NULL) { - output = xmlCtxtReadFile(ctxt, filename, NULL, - PCMK__XML_PARSE_OPTS_WITH_RECOVER); - if (output) { - crm_warn("Successfully recovered from XML errors " - "(note: a future release will treat this as a fatal failure)"); - } - } - - } else { - char *input = decompress_file(filename); - - output = xmlCtxtReadDoc(ctxt, (pcmkXmlStr) input, NULL, NULL, - PCMK__XML_PARSE_OPTS_WITHOUT_RECOVER); - - if (output == NULL) { - output = xmlCtxtReadDoc(ctxt, (pcmkXmlStr) input, NULL, NULL, - PCMK__XML_PARSE_OPTS_WITH_RECOVER); - if (output) { - crm_warn("Successfully recovered from XML errors " - "(note: a future release will treat this as a fatal failure)"); - } - } - - free(input); - } - - if (output && (xml = xmlDocGetRootElement(output))) { - pcmk__strip_xml_text(xml); - } - - last_error = xmlCtxtGetLastError(ctxt); - if (last_error && last_error->code != XML_ERR_OK) { - /* crm_abort(__FILE__,__func__,__LINE__, "last_error->code != XML_ERR_OK", TRUE, TRUE); */ - /* - * http://xmlsoft.org/html/libxml-xmlerror.html#xmlErrorLevel - * http://xmlsoft.org/html/libxml-xmlerror.html#xmlParserErrors - */ - crm_err("Parsing failed (domain=%d, level=%d, code=%d): %s", - last_error->domain, last_error->level, last_error->code, last_error->message); - - if (last_error && last_error->code != XML_ERR_OK) { - crm_err("Couldn't%s parse %s", xml ? " fully" : "", filename); - if (xml != NULL) { - crm_log_xml_err(xml, "Partial"); - } - } - } - - xmlFreeParserCtxt(ctxt); - return xml; -} - /*! * \internal * \brief Add a "last written" attribute to an XML element, set to current time @@ -1091,7 +961,7 @@ pcmk__xe_add_last_written(xmlNode *xe) char *now_s = pcmk__epoch2str(NULL, 0); const char *result = NULL; - result = crm_xml_add(xe, XML_CIB_ATTR_WRITTEN, + result = crm_xml_add(xe, PCMK_XA_CIB_LAST_WRITTEN, pcmk__s(now_s, "Could not determine current time")); free(now_s); return result; @@ -1138,598 +1008,202 @@ crm_xml_set_id(xmlNode *xml, const char *format, ...) CRM_ASSERT(len > 0); crm_xml_sanitize_id(id); - crm_xml_add(xml, XML_ATTR_ID, id); + crm_xml_add(xml, PCMK_XA_ID, id); free(id); } /*! * \internal - * \brief Write XML to a file stream + * \brief Check whether a string has XML special characters that must be escaped * - * \param[in] xml XML to write - * \param[in] filename Name of file being written (for logging only) - * \param[in,out] stream Open file stream corresponding to filename - * \param[in] compress Whether to compress XML before writing - * \param[out] nbytes Number of bytes written + * See \c pcmk__xml_escape() and \c pcmk__xml_escape_type for more details. * - * \return Standard Pacemaker return code - */ -static int -write_xml_stream(const xmlNode *xml, const char *filename, FILE *stream, - bool compress, unsigned int *nbytes) -{ - int rc = pcmk_rc_ok; - char *buffer = NULL; - - *nbytes = 0; - crm_log_xml_trace(xml, "writing"); - - buffer = dump_xml_formatted(xml); - CRM_CHECK(buffer && strlen(buffer), - crm_log_xml_warn(xml, "formatting failed"); - rc = pcmk_rc_error; - goto bail); - - if (compress) { - unsigned int in = 0; - BZFILE *bz_file = NULL; - - rc = BZ_OK; - bz_file = BZ2_bzWriteOpen(&rc, stream, 5, 0, 30); - rc = pcmk__bzlib2rc(rc); - - if (rc != pcmk_rc_ok) { - crm_warn("Not compressing %s: could not prepare file stream: %s " - CRM_XS " rc=%d", filename, pcmk_rc_str(rc), rc); - } else { - BZ2_bzWrite(&rc, bz_file, buffer, strlen(buffer)); - rc = pcmk__bzlib2rc(rc); - - if (rc != pcmk_rc_ok) { - crm_warn("Not compressing %s: could not compress data: %s " - CRM_XS " rc=%d errno=%d", - filename, pcmk_rc_str(rc), rc, errno); - } - } - - if (rc == pcmk_rc_ok) { - BZ2_bzWriteClose(&rc, bz_file, 0, &in, nbytes); - rc = pcmk__bzlib2rc(rc); - - if (rc != pcmk_rc_ok) { - crm_warn("Not compressing %s: could not write compressed data: %s " - CRM_XS " rc=%d errno=%d", - filename, pcmk_rc_str(rc), rc, errno); - *nbytes = 0; // retry without compression - } else { - crm_trace("Compressed XML for %s from %u bytes to %u", - filename, in, *nbytes); - } - } - rc = pcmk_rc_ok; // Either true, or we'll retry without compression - } - - if (*nbytes == 0) { - rc = fprintf(stream, "%s", buffer); - if (rc < 0) { - rc = errno; - crm_perror(LOG_ERR, "writing %s", filename); - } else { - *nbytes = (unsigned int) rc; - rc = pcmk_rc_ok; - } - } - - bail: - - if (fflush(stream) != 0) { - rc = errno; - crm_perror(LOG_ERR, "flushing %s", filename); - } - - /* Don't report error if the file does not support synchronization */ - if (fsync(fileno(stream)) < 0 && errno != EROFS && errno != EINVAL) { - rc = errno; - crm_perror(LOG_ERR, "synchronizing %s", filename); - } - - fclose(stream); - - crm_trace("Saved %d bytes to %s as XML", *nbytes, filename); - free(buffer); - - return rc; -} - -/*! - * \brief Write XML to a file descriptor - * - * \param[in] xml XML to write - * \param[in] filename Name of file being written (for logging only) - * \param[in] fd Open file descriptor corresponding to filename - * \param[in] compress Whether to compress XML before writing + * \param[in] text String to check + * \param[in] type Type of escaping * - * \return Number of bytes written on success, -errno otherwise + * \return \c true if \p text has special characters that need to be escaped, or + * \c false otherwise */ -int -write_xml_fd(const xmlNode *xml, const char *filename, int fd, - gboolean compress) -{ - FILE *stream = NULL; - unsigned int nbytes = 0; - int rc = pcmk_rc_ok; - - CRM_CHECK((xml != NULL) && (fd > 0), return -EINVAL); - stream = fdopen(fd, "w"); - if (stream == NULL) { - return -errno; - } - rc = write_xml_stream(xml, filename, stream, compress, &nbytes); - if (rc != pcmk_rc_ok) { - return pcmk_rc2legacy(rc); - } - return (int) nbytes; -} - -/*! - * \brief Write XML to a file - * - * \param[in] xml XML to write - * \param[in] filename Name of file to write - * \param[in] compress Whether to compress XML before writing - * - * \return Number of bytes written on success, -errno otherwise - */ -int -write_xml_file(const xmlNode *xml, const char *filename, gboolean compress) +bool +pcmk__xml_needs_escape(const char *text, enum pcmk__xml_escape_type type) { - FILE *stream = NULL; - unsigned int nbytes = 0; - int rc = pcmk_rc_ok; + if (text == NULL) { + return false; + } + + while (*text != '\0') { + switch (type) { + case pcmk__xml_escape_text: + switch (*text) { + case '<': + case '>': + case '&': + return true; + case '\n': + case '\t': + break; + default: + if (g_ascii_iscntrl(*text)) { + return true; + } + break; + } + break; - CRM_CHECK((xml != NULL) && (filename != NULL), return -EINVAL); - stream = fopen(filename, "w"); - if (stream == NULL) { - return -errno; - } - rc = write_xml_stream(xml, filename, stream, compress, &nbytes); - if (rc != pcmk_rc_ok) { - return pcmk_rc2legacy(rc); - } - return (int) nbytes; -} + case pcmk__xml_escape_attr: + switch (*text) { + case '<': + case '>': + case '&': + case '"': + return true; + default: + if (g_ascii_iscntrl(*text)) { + return true; + } + break; + } + break; -// Replace a portion of a dynamically allocated string (reallocating memory) -static char * -replace_text(char *text, int start, size_t *length, const char *replace) -{ - size_t offset = strlen(replace) - 1; // We have space for 1 char already + case pcmk__xml_escape_attr_pretty: + switch (*text) { + case '\n': + case '\r': + case '\t': + case '"': + return true; + default: + break; + } + break; - *length += offset; - text = pcmk__realloc(text, *length); + default: // Invalid enum value + CRM_ASSERT(false); + break; + } - for (size_t lpc = (*length) - 1; lpc > (start + offset); lpc--) { - text[lpc] = text[lpc - offset]; + text = g_utf8_next_char(text); } - - memcpy(text + start, replace, offset + 1); - return text; + return false; } /*! + * \internal * \brief Replace special characters with their XML escape sequences * * \param[in] text Text to escape + * \param[in] type Type of escaping * * \return Newly allocated string equivalent to \p text but with special - * characters replaced with XML escape sequences (or NULL if \p text - * is NULL) + * characters replaced with XML escape sequences (or \c NULL if \p text + * is \c NULL). If \p text is not \c NULL, the return value is + * guaranteed not to be \c NULL. + * + * \note There are libxml functions that purport to do this: + * \c xmlEncodeEntitiesReentrant() and \c xmlEncodeSpecialChars(). + * However, their escaping is incomplete. See: + * https://discourse.gnome.org/t/intended-use-of-xmlencodeentitiesreentrant-vs-xmlencodespecialchars/19252 + * \note The caller is responsible for freeing the return value using + * \c g_free(). */ -char * -crm_xml_escape(const char *text) +gchar * +pcmk__xml_escape(const char *text, enum pcmk__xml_escape_type type) { - size_t length; - char *copy; - - /* - * When xmlCtxtReadDoc() parses < and friends in a - * value, it converts them to their human readable - * form. - * - * If one uses xmlNodeDump() to convert it back to a - * string, all is well, because special characters are - * converted back to their escape sequences. - * - * However xmlNodeDump() is randomly dog slow, even with the same - * input. So we need to replicate the escaping in our custom - * version so that the result can be re-parsed by xmlCtxtReadDoc() - * when necessary. - */ + GString *copy = NULL; if (text == NULL) { return NULL; } + copy = g_string_sized_new(strlen(text)); - length = 1 + strlen(text); - copy = strdup(text); - CRM_ASSERT(copy != NULL); - for (size_t index = 0; index < length; index++) { - if(copy[index] & 0x80 && copy[index+1] & 0x80){ - index++; - break; - } - switch (copy[index]) { - case 0: - break; - case '<': - copy = replace_text(copy, index, &length, "<"); - break; - case '>': - copy = replace_text(copy, index, &length, ">"); - break; - case '"': - copy = replace_text(copy, index, &length, """); - break; - case '\'': - copy = replace_text(copy, index, &length, "'"); - break; - case '&': - copy = replace_text(copy, index, &length, "&"); - break; - case '\t': - /* Might as well just expand to a few spaces... */ - copy = replace_text(copy, index, &length, " "); - break; - case '\n': - copy = replace_text(copy, index, &length, "\\n"); - break; - case '\r': - copy = replace_text(copy, index, &length, "\\r"); - break; - default: - /* Check for and replace non-printing characters with their octal equivalent */ - if(copy[index] < ' ' || copy[index] > '~') { - char *replace = crm_strdup_printf("\\%.3o", copy[index]); + while (*text != '\0') { + // Don't escape any non-ASCII characters + if ((*text & 0x80) != 0) { + size_t bytes = g_utf8_next_char(text) - text; - copy = replace_text(copy, index, &length, replace); - free(replace); - } - } - } - return copy; -} - -/*! - * \internal - * \brief Append a string representation of an XML element to a buffer - * - * \param[in] data XML whose representation to append - * \param[in] options Group of \p pcmk__xml_fmt_options flags - * \param[in,out] buffer Where to append the content (must not be \p NULL) - * \param[in] depth Current indentation level - */ -static void -dump_xml_element(const xmlNode *data, uint32_t options, GString *buffer, - int depth) -{ - bool pretty = pcmk_is_set(options, pcmk__xml_fmt_pretty); - bool filtered = pcmk_is_set(options, pcmk__xml_fmt_filtered); - int spaces = pretty? (2 * depth) : 0; - - for (int lpc = 0; lpc < spaces; lpc++) { - g_string_append_c(buffer, ' '); - } - - pcmk__g_strcat(buffer, "<", data->name, NULL); - - for (const xmlAttr *attr = pcmk__xe_first_attr(data); attr != NULL; - attr = attr->next) { - - if (!filtered || !pcmk__xa_filterable((const char *) (attr->name))) { - pcmk__dump_xml_attr(attr, buffer); + g_string_append_len(copy, text, bytes); + text += bytes; + continue; } - } - - if (data->children == NULL) { - g_string_append(buffer, "/>"); - - } else { - g_string_append_c(buffer, '>'); - } - if (pretty) { - g_string_append_c(buffer, '\n'); - } - - if (data->children) { - for (const xmlNode *child = data->children; child != NULL; - child = child->next) { - pcmk__xml2text(child, options, buffer, depth + 1); - } + switch (type) { + case pcmk__xml_escape_text: + switch (*text) { + case '<': + g_string_append(copy, PCMK__XML_ENTITY_LT); + break; + case '>': + g_string_append(copy, PCMK__XML_ENTITY_GT); + break; + case '&': + g_string_append(copy, PCMK__XML_ENTITY_AMP); + break; + case '\n': + case '\t': + g_string_append_c(copy, *text); + break; + default: + if (g_ascii_iscntrl(*text)) { + g_string_append_printf(copy, "&#x%.2X;", *text); + } else { + g_string_append_c(copy, *text); + } + break; + } + break; - for (int lpc = 0; lpc < spaces; lpc++) { - g_string_append_c(buffer, ' '); - } + case pcmk__xml_escape_attr: + switch (*text) { + case '<': + g_string_append(copy, PCMK__XML_ENTITY_LT); + break; + case '>': + g_string_append(copy, PCMK__XML_ENTITY_GT); + break; + case '&': + g_string_append(copy, PCMK__XML_ENTITY_AMP); + break; + case '"': + g_string_append(copy, PCMK__XML_ENTITY_QUOT); + break; + default: + if (g_ascii_iscntrl(*text)) { + g_string_append_printf(copy, "&#x%.2X;", *text); + } else { + g_string_append_c(copy, *text); + } + break; + } + break; - pcmk__g_strcat(buffer, "</", data->name, ">", NULL); + case pcmk__xml_escape_attr_pretty: + switch (*text) { + case '"': + g_string_append(copy, "\\\""); + break; + case '\n': + g_string_append(copy, "\\n"); + break; + case '\r': + g_string_append(copy, "\\r"); + break; + case '\t': + g_string_append(copy, "\\t"); + break; + default: + g_string_append_c(copy, *text); + break; + } + break; - if (pretty) { - g_string_append_c(buffer, '\n'); + default: // Invalid enum value + CRM_ASSERT(false); + break; } - } -} - -/*! - * \internal - * \brief Append XML text content to a buffer - * - * \param[in] data XML whose content to append - * \param[in] options Group of \p xml_log_options flags - * \param[in,out] buffer Where to append the content (must not be \p NULL) - * \param[in] depth Current indentation level - */ -static void -dump_xml_text(const xmlNode *data, uint32_t options, GString *buffer, - int depth) -{ - /* @COMPAT: Remove when log_data_element() is removed. There are no internal - * code paths to this, except through the deprecated log_data_element(). - */ - bool pretty = pcmk_is_set(options, pcmk__xml_fmt_pretty); - int spaces = pretty? (2 * depth) : 0; - - for (int lpc = 0; lpc < spaces; lpc++) { - g_string_append_c(buffer, ' '); - } - - g_string_append(buffer, (const gchar *) data->content); - - if (pretty) { - g_string_append_c(buffer, '\n'); - } -} - -/*! - * \internal - * \brief Append XML CDATA content to a buffer - * - * \param[in] data XML whose content to append - * \param[in] options Group of \p pcmk__xml_fmt_options flags - * \param[in,out] buffer Where to append the content (must not be \p NULL) - * \param[in] depth Current indentation level - */ -static void -dump_xml_cdata(const xmlNode *data, uint32_t options, GString *buffer, - int depth) -{ - bool pretty = pcmk_is_set(options, pcmk__xml_fmt_pretty); - int spaces = pretty? (2 * depth) : 0; - - for (int lpc = 0; lpc < spaces; lpc++) { - g_string_append_c(buffer, ' '); - } - - pcmk__g_strcat(buffer, "<![CDATA[", (const char *) data->content, "]]>", - NULL); - if (pretty) { - g_string_append_c(buffer, '\n'); + text = g_utf8_next_char(text); } -} - -/*! - * \internal - * \brief Append an XML comment to a buffer - * - * \param[in] data XML whose content to append - * \param[in] options Group of \p pcmk__xml_fmt_options flags - * \param[in,out] buffer Where to append the content (must not be \p NULL) - * \param[in] depth Current indentation level - */ -static void -dump_xml_comment(const xmlNode *data, uint32_t options, GString *buffer, - int depth) -{ - bool pretty = pcmk_is_set(options, pcmk__xml_fmt_pretty); - int spaces = pretty? (2 * depth) : 0; - - for (int lpc = 0; lpc < spaces; lpc++) { - g_string_append_c(buffer, ' '); - } - - pcmk__g_strcat(buffer, "<!--", (const char *) data->content, "-->", NULL); - - if (pretty) { - g_string_append_c(buffer, '\n'); - } -} - -/*! - * \internal - * \brief Get a string representation of an XML element type - * - * \param[in] type XML element type - * - * \return String representation of \p type - */ -static const char * -xml_element_type2str(xmlElementType type) -{ - static const char *const element_type_names[] = { - [XML_ELEMENT_NODE] = "element", - [XML_ATTRIBUTE_NODE] = "attribute", - [XML_TEXT_NODE] = "text", - [XML_CDATA_SECTION_NODE] = "CDATA section", - [XML_ENTITY_REF_NODE] = "entity reference", - [XML_ENTITY_NODE] = "entity", - [XML_PI_NODE] = "PI", - [XML_COMMENT_NODE] = "comment", - [XML_DOCUMENT_NODE] = "document", - [XML_DOCUMENT_TYPE_NODE] = "document type", - [XML_DOCUMENT_FRAG_NODE] = "document fragment", - [XML_NOTATION_NODE] = "notation", - [XML_HTML_DOCUMENT_NODE] = "HTML document", - [XML_DTD_NODE] = "DTD", - [XML_ELEMENT_DECL] = "element declaration", - [XML_ATTRIBUTE_DECL] = "attribute declaration", - [XML_ENTITY_DECL] = "entity declaration", - [XML_NAMESPACE_DECL] = "namespace declaration", - [XML_XINCLUDE_START] = "XInclude start", - [XML_XINCLUDE_END] = "XInclude end", - }; - - if ((type < 0) || (type >= PCMK__NELEM(element_type_names))) { - return "unrecognized type"; - } - return element_type_names[type]; -} - -/*! - * \internal - * \brief Create a text representation of an XML object - * - * \param[in] data XML to convert - * \param[in] options Group of \p pcmk__xml_fmt_options flags - * \param[in,out] buffer Where to store the text (must not be \p NULL) - * \param[in] depth Current indentation level - */ -void -pcmk__xml2text(const xmlNode *data, uint32_t options, GString *buffer, - int depth) -{ - if (data == NULL) { - crm_trace("Nothing to dump"); - return; - } - - CRM_ASSERT(buffer != NULL); - CRM_CHECK(depth >= 0, depth = 0); - - switch(data->type) { - case XML_ELEMENT_NODE: - /* Handle below */ - dump_xml_element(data, options, buffer, depth); - break; - case XML_TEXT_NODE: - if (pcmk_is_set(options, pcmk__xml_fmt_text)) { - dump_xml_text(data, options, buffer, depth); - } - break; - case XML_COMMENT_NODE: - dump_xml_comment(data, options, buffer, depth); - break; - case XML_CDATA_SECTION_NODE: - dump_xml_cdata(data, options, buffer, depth); - break; - default: - crm_warn("Cannot convert XML %s node to text " CRM_XS " type=%d", - xml_element_type2str(data->type), data->type); - break; - } -} - -char * -dump_xml_formatted_with_text(const xmlNode *xml) -{ - /* libxml's xmlNodeDumpOutput() would work here since we're not specifically - * filtering out any nodes. However, use pcmk__xml2text() for consistency, - * to escape attribute values, and to allow a const argument. - */ - char *buffer = NULL; - GString *g_buffer = g_string_sized_new(1024); - - pcmk__xml2text(xml, pcmk__xml_fmt_pretty|pcmk__xml_fmt_text, g_buffer, 0); - - pcmk__str_update(&buffer, g_buffer->str); - g_string_free(g_buffer, TRUE); - return buffer; -} - -char * -dump_xml_formatted(const xmlNode *xml) -{ - char *buffer = NULL; - GString *g_buffer = g_string_sized_new(1024); - - pcmk__xml2text(xml, pcmk__xml_fmt_pretty, g_buffer, 0); - - pcmk__str_update(&buffer, g_buffer->str); - g_string_free(g_buffer, TRUE); - return buffer; -} - -char * -dump_xml_unformatted(const xmlNode *xml) -{ - char *buffer = NULL; - GString *g_buffer = g_string_sized_new(1024); - - pcmk__xml2text(xml, 0, g_buffer, 0); - - pcmk__str_update(&buffer, g_buffer->str); - g_string_free(g_buffer, TRUE); - return buffer; -} - -int -pcmk__xml2fd(int fd, xmlNode *cur) -{ - bool success; - - xmlOutputBuffer *fd_out = xmlOutputBufferCreateFd(fd, NULL); - CRM_ASSERT(fd_out != NULL); - xmlNodeDumpOutput(fd_out, cur->doc, cur, 0, pcmk__xml_fmt_pretty, NULL); - - success = xmlOutputBufferWrite(fd_out, sizeof("\n") - 1, "\n") != -1; - - success = xmlOutputBufferClose(fd_out) != -1 && success; - - if (!success) { - return EIO; - } - - fsync(fd); - return pcmk_rc_ok; -} - -void -xml_remove_prop(xmlNode * obj, const char *name) -{ - if (crm_element_value(obj, name) == NULL) { - return; - } - - if (pcmk__check_acl(obj, NULL, pcmk__xf_acl_write) == FALSE) { - crm_trace("Cannot remove %s from %s", name, obj->name); - - } else if (pcmk__tracking_xml_changes(obj, FALSE)) { - /* Leave in place (marked for removal) until after the diff is calculated */ - xmlAttr *attr = xmlHasProp(obj, (pcmkXmlStr) name); - xml_node_private_t *nodepriv = attr->_private; - - set_parent_flag(obj, pcmk__xf_dirty); - pcmk__set_xml_flags(nodepriv, pcmk__xf_deleted); - } else { - xmlUnsetProp(obj, (pcmkXmlStr) name); - } -} - -void -save_xml_to_file(const xmlNode *xml, const char *desc, const char *filename) -{ - char *f = NULL; - - if (filename == NULL) { - char *uuid = crm_generate_uuid(); - - f = crm_strdup_printf("%s/%s", pcmk__get_tmpdir(), uuid); - filename = f; - free(uuid); - } - - crm_info("Saving %s to %s", desc, filename); - write_xml_file(xml, filename, FALSE); - free(f); + return g_string_free(copy, FALSE); } /*! @@ -1781,7 +1255,7 @@ mark_attr_deleted(xmlNode *new_xml, const char *element, const char *attr_name, nodepriv->flags = 0; // Check ACLs and mark restored value for later removal - xml_remove_prop(new_xml, attr_name); + remove_xe_attr(new_xml, attr); crm_trace("XML attribute %s=%s was removed from %s", attr_name, old_value, element); @@ -1955,10 +1429,10 @@ static void mark_child_deleted(xmlNode *old_child, xmlNode *new_parent) { // Re-create the child element so we can check ACLs - xmlNode *candidate = add_node_copy(new_parent, old_child); + xmlNode *candidate = pcmk__xml_copy(new_parent, old_child); // Clear flags on new child and its children - reset_xml_node_flags(candidate); + pcmk__xml_tree_foreach(candidate, reset_xml_node_flags, NULL); // Check whether ACLs allow the deletion pcmk__apply_acl(xmlDocGetRootElement(candidate->doc)); @@ -1979,8 +1453,9 @@ mark_child_moved(xmlNode *old_child, xmlNode *new_parent, xmlNode *new_child, { xml_node_private_t *nodepriv = new_child->_private; - crm_trace("Child element %s with id='%s' moved from position %d to %d under %s", - new_child->name, (ID(new_child)? ID(new_child) : "<no id>"), + crm_trace("Child element %s with " + PCMK_XA_ID "='%s' moved from position %d to %d under %s", + new_child->name, pcmk__s(pcmk__xe_id(new_child), "<no id>"), p_old, p_new, new_parent->name); pcmk__mark_xml_node_dirty(new_parent); pcmk__set_xml_flags(nodepriv, pcmk__xf_moved); @@ -1997,12 +1472,13 @@ mark_child_moved(xmlNode *old_child, xmlNode *new_parent, xmlNode *new_child, static void mark_xml_changes(xmlNode *old_xml, xmlNode *new_xml, bool check_top) { - xmlNode *cIter = NULL; + xmlNode *old_child = NULL; + xmlNode *new_child = NULL; xml_node_private_t *nodepriv = NULL; CRM_CHECK(new_xml != NULL, return); if (old_xml == NULL) { - pcmk__mark_xml_created(new_xml); + pcmk__xml_mark_created(new_xml); pcmk__apply_creation_acl(new_xml, check_top); return; } @@ -2019,13 +1495,13 @@ mark_xml_changes(xmlNode *old_xml, xmlNode *new_xml, bool check_top) xml_diff_attrs(old_xml, new_xml); // Check for differences in the original children - for (cIter = pcmk__xml_first_child(old_xml); cIter != NULL; ) { - xmlNode *old_child = cIter; - xmlNode *new_child = pcmk__xml_match(new_xml, cIter, true); + for (old_child = pcmk__xml_first_child(old_xml); old_child != NULL; + old_child = pcmk__xml_next(old_child)) { + + new_child = pcmk__xml_match(new_xml, old_child, true); - cIter = pcmk__xml_next(cIter); - if(new_child) { - mark_xml_changes(old_child, new_child, TRUE); + if (new_child != NULL) { + mark_xml_changes(old_child, new_child, true); } else { mark_child_deleted(old_child, new_xml); @@ -2033,16 +1509,19 @@ mark_xml_changes(xmlNode *old_xml, xmlNode *new_xml, bool check_top) } // Check for moved or created children - for (cIter = pcmk__xml_first_child(new_xml); cIter != NULL; ) { - xmlNode *new_child = cIter; - xmlNode *old_child = pcmk__xml_match(old_xml, cIter, true); + new_child = pcmk__xml_first_child(new_xml); + while (new_child != NULL) { + xmlNode *next = pcmk__xml_next(new_child); + + old_child = pcmk__xml_match(old_xml, new_child, true); - cIter = pcmk__xml_next(cIter); - if(old_child == NULL) { + if (old_child == NULL) { // This is a newly created child nodepriv = new_child->_private; pcmk__set_xml_flags(nodepriv, pcmk__xf_skip); - mark_xml_changes(old_child, new_child, TRUE); + + // May free new_child + mark_xml_changes(old_child, new_child, true); } else { /* Check for movement, we already checked for differences */ @@ -2053,6 +1532,8 @@ mark_xml_changes(xmlNode *old_xml, xmlNode *new_xml, bool check_top) mark_child_moved(old_child, new_xml, new_child, p_old, p_new); } } + + new_child = next; } } @@ -2069,7 +1550,8 @@ xml_calculate_changes(xmlNode *old_xml, xmlNode *new_xml) { CRM_CHECK((old_xml != NULL) && (new_xml != NULL) && pcmk__xe_is(old_xml, (const char *) new_xml->name) - && pcmk__str_eq(ID(old_xml), ID(new_xml), pcmk__str_none), + && pcmk__str_eq(pcmk__xe_id(old_xml), pcmk__xe_id(new_xml), + pcmk__str_none), return); if(xml_tracking_changes(new_xml) == FALSE) { @@ -2079,44 +1561,6 @@ xml_calculate_changes(xmlNode *old_xml, xmlNode *new_xml) mark_xml_changes(old_xml, new_xml, FALSE); } -gboolean -can_prune_leaf(xmlNode * xml_node) -{ - xmlNode *cIter = NULL; - gboolean can_prune = TRUE; - - CRM_CHECK(xml_node != NULL, return FALSE); - - if (pcmk__strcase_any_of((const char *) xml_node->name, - XML_TAG_RESOURCE_REF, XML_CIB_TAG_OBJ_REF, - XML_ACL_TAG_ROLE_REF, XML_ACL_TAG_ROLE_REFv1, - NULL)) { - return FALSE; - } - - for (xmlAttrPtr a = pcmk__xe_first_attr(xml_node); a != NULL; a = a->next) { - const char *p_name = (const char *) a->name; - - if (strcmp(p_name, XML_ATTR_ID) == 0) { - continue; - } - can_prune = FALSE; - } - - cIter = pcmk__xml_first_child(xml_node); - while (cIter) { - xmlNode *child = cIter; - - cIter = pcmk__xml_next(cIter); - if (can_prune_leaf(child)) { - free_xml(child); - } else { - can_prune = FALSE; - } - } - return can_prune; -} - /*! * \internal * \brief Find a comment with matching content in specified XML @@ -2185,7 +1629,7 @@ pcmk__xc_update(xmlNode *parent, xmlNode *target, xmlNode *update) } if (target == NULL) { - add_node_copy(parent, update); + pcmk__xml_copy(parent, update); } else if (!pcmk__str_eq((const char *)target->content, (const char *)update->content, pcmk__str_casei)) { xmlFree(target->content); @@ -2195,82 +1639,113 @@ pcmk__xc_update(xmlNode *parent, xmlNode *target, xmlNode *update) /*! * \internal - * \brief Make one XML tree match another (in children and attributes) + * \brief Merge one XML tree into another + * + * Here, "merge" means: + * 1. Copy attribute values from \p update to the target, overwriting in case of + * conflict. + * 2. Descend through \p update and the target in parallel. At each level, for + * each child of \p update, look for a matching child of the target. + * a. For each child, if a match is found, go to step 1, recursively merging + * the child of \p update into the child of the target. + * b. Otherwise, copy the child of \p update as a child of the target. + * + * A match is defined as the first child of the same type within the target, + * with: + * * the \c PCMK_XA_ID attribute matching, if set in \p update; otherwise, + * * the \c PCMK_XA_ID_REF attribute matching, if set in \p update + * + * This function does not delete any elements or attributes from the target. It + * may add elements or overwrite attributes, as described above. * * \param[in,out] parent If \p target is NULL and this is not, add or update * child of this XML node that matches \p update * \param[in,out] target If not NULL, update this XML - * \param[in] update Make the desired XML match this (must not be NULL) - * \param[in] as_diff If false, expand "++" when making attributes match + * \param[in] update Make the desired XML match this (must not be \c NULL) + * \param[in] flags Group of <tt>enum pcmk__xa_flags</tt> + * \param[in] as_diff If \c true, preserve order of attributes (deprecated + * since 2.0.5) * - * \note At least one of \p parent and \p target must be non-NULL + * \note At least one of \p parent and \p target must be non-<tt>NULL</tt>. + * \note This function is recursive. For the top-level call, \p parent is + * \c NULL and \p target is not \c NULL. For recursive calls, \p target is + * \c NULL and \p parent is not \c NULL. */ void pcmk__xml_update(xmlNode *parent, xmlNode *target, xmlNode *update, - bool as_diff) + uint32_t flags, bool as_diff) { - xmlNode *a_child = NULL; - const char *object_name = NULL, - *object_href = NULL, - *object_href_val = NULL; + /* @COMPAT Refactor further and staticize after v1 patchset deprecation. + * + * @COMPAT Drop as_diff argument when apply_xml_diff() is dropped. + */ + const char *update_name = NULL; + const char *update_id_attr = NULL; + const char *update_id_val = NULL; + char *trace_s = NULL; -#if XML_PARSER_DEBUG - crm_log_xml_trace(update, "update:"); - crm_log_xml_trace(target, "target:"); -#endif + crm_log_xml_trace(update, "update"); + crm_log_xml_trace(target, "target"); - CRM_CHECK(update != NULL, return); + CRM_CHECK(update != NULL, goto done); if (update->type == XML_COMMENT_NODE) { pcmk__xc_update(parent, target, update); - return; + goto done; } - object_name = (const char *) update->name; - object_href_val = ID(update); - if (object_href_val != NULL) { - object_href = XML_ATTR_ID; + update_name = (const char *) update->name; + + CRM_CHECK(update_name != NULL, goto done); + CRM_CHECK((target != NULL) || (parent != NULL), goto done); + + update_id_val = pcmk__xe_id(update); + if (update_id_val != NULL) { + update_id_attr = PCMK_XA_ID; + } else { - object_href_val = crm_element_value(update, XML_ATTR_IDREF); - object_href = (object_href_val == NULL) ? NULL : XML_ATTR_IDREF; + update_id_val = crm_element_value(update, PCMK_XA_ID_REF); + if (update_id_val != NULL) { + update_id_attr = PCMK_XA_ID_REF; + } } - CRM_CHECK(object_name != NULL, return); - CRM_CHECK(target != NULL || parent != NULL, return); + pcmk__if_tracing( + { + if (update_id_attr != NULL) { + trace_s = crm_strdup_printf("<%s %s=%s/>", + update_name, update_id_attr, + update_id_val); + } else { + trace_s = crm_strdup_printf("<%s/>", update_name); + } + }, + {} + ); if (target == NULL) { - target = pcmk__xe_match(parent, object_name, - object_href, object_href_val); + // Recursive call + target = pcmk__xe_first_child(parent, update_name, update_id_attr, + update_id_val); } if (target == NULL) { - target = create_xml_node(parent, object_name); - CRM_CHECK(target != NULL, return); -#if XML_PARSER_DEBUG - crm_trace("Added <%s%s%s%s%s/>", pcmk__s(object_name, "<null>"), - object_href ? " " : "", - object_href ? object_href : "", - object_href ? "=" : "", - object_href ? object_href_val : ""); + // Recursive call with no existing matching child + target = pcmk__xe_create(parent, update_name); + crm_trace("Added %s", pcmk__s(trace_s, update_name)); } else { - crm_trace("Found node <%s%s%s%s%s/> to update", - pcmk__s(object_name, "<null>"), - object_href ? " " : "", - object_href ? object_href : "", - object_href ? "=" : "", - object_href ? object_href_val : ""); -#endif + // Either recursive call with match, or top-level call + crm_trace("Found node %s to update", pcmk__s(trace_s, update_name)); } CRM_CHECK(pcmk__xe_is(target, (const char *) update->name), return); - if (as_diff == FALSE) { - /* So that expand_plus_plus() gets called */ - copy_in_properties(target, update); + if (!as_diff) { + pcmk__xe_copy_attrs(target, update, flags); } else { - /* No need for expand_plus_plus(), just raw speed */ + // Preserve order of attributes. Don't use pcmk__xe_copy_attrs(). for (xmlAttrPtr a = pcmk__xe_first_attr(update); a != NULL; a = a->next) { const char *p_value = pcmk__xml_attr_value(a); @@ -2281,175 +1756,316 @@ pcmk__xml_update(xmlNode *parent, xmlNode *target, xmlNode *update, } } - for (a_child = pcmk__xml_first_child(update); a_child != NULL; - a_child = pcmk__xml_next(a_child)) { -#if XML_PARSER_DEBUG - crm_trace("Updating child <%s%s%s%s%s/>", - pcmk__s(object_name, "<null>"), - object_href ? " " : "", - object_href ? object_href : "", - object_href ? "=" : "", - object_href ? object_href_val : ""); -#endif - pcmk__xml_update(target, NULL, a_child, as_diff); - } - -#if XML_PARSER_DEBUG - crm_trace("Finished with <%s%s%s%s%s/>", pcmk__s(object_name, "<null>"), - object_href ? " " : "", - object_href ? object_href : "", - object_href ? "=" : "", - object_href ? object_href_val : ""); -#endif -} + for (xmlNode *child = pcmk__xml_first_child(update); child != NULL; + child = pcmk__xml_next(child)) { -gboolean -update_xml_child(xmlNode * child, xmlNode * to_update) -{ - gboolean can_update = TRUE; - xmlNode *child_of_child = NULL; + crm_trace("Updating child of %s", pcmk__s(trace_s, update_name)); + pcmk__xml_update(target, NULL, child, flags, as_diff); + } - CRM_CHECK(child != NULL, return FALSE); - CRM_CHECK(to_update != NULL, return FALSE); + crm_trace("Finished with %s", pcmk__s(trace_s, update_name)); - if (!pcmk__xe_is(to_update, (const char *) child->name)) { - can_update = FALSE; +done: + free(trace_s); +} - } else if (!pcmk__str_eq(ID(to_update), ID(child), pcmk__str_none)) { - can_update = FALSE; +/*! + * \internal + * \brief Delete an XML subtree if it matches a search element + * + * A match is defined as follows: + * * \p xml and \p user_data are both element nodes of the same type. + * * If \p user_data has attributes set, \p xml has those attributes set to the + * same values. (\p xml may have additional attributes set to arbitrary + * values.) + * + * \param[in,out] xml XML subtree to delete upon match + * \param[in] user_data Search element + * + * \return \c true to continue traversing the tree, or \c false to stop (because + * \p xml was deleted) + * + * \note This is compatible with \c pcmk__xml_tree_foreach(). + */ +static bool +delete_xe_if_matching(xmlNode *xml, void *user_data) +{ + xmlNode *search = user_data; - } else if (can_update) { -#if XML_PARSER_DEBUG - crm_log_xml_trace(child, "Update match found..."); -#endif - pcmk__xml_update(NULL, child, to_update, false); + if (!pcmk__xe_is(search, (const char *) xml->name)) { + // No match: either not both elements, or different element types + return true; } - for (child_of_child = pcmk__xml_first_child(child); child_of_child != NULL; - child_of_child = pcmk__xml_next(child_of_child)) { - /* only update the first one */ - if (can_update) { - break; + for (const xmlAttr *attr = pcmk__xe_first_attr(search); attr != NULL; + attr = attr->next) { + + const char *search_val = pcmk__xml_attr_value(attr); + const char *xml_val = crm_element_value(xml, (const char *) attr->name); + + if (!pcmk__str_eq(search_val, xml_val, pcmk__str_casei)) { + // No match: an attr in xml doesn't match the attr in search + return true; } - can_update = update_xml_child(child_of_child, to_update); } - return can_update; + crm_log_xml_trace(xml, "delete-match"); + crm_log_xml_trace(search, "delete-search"); + free_xml(xml); + + // Found a match and deleted it; stop traversing tree + return false; } +/*! + * \internal + * \brief Search an XML tree depth-first and delete the first matching element + * + * This function does not attempt to match the tree root (\p xml). + * + * A match with a node \c node is defined as follows: + * * \c node and \p search are both element nodes of the same type. + * * If \p search has attributes set, \c node has those attributes set to the + * same values. (\c node may have additional attributes set to arbitrary + * values.) + * + * \param[in,out] xml XML subtree to search + * \param[in] search Element to match against + * + * \return Standard Pacemaker return code (specifically, \c pcmk_rc_ok on + * successful deletion and an error code otherwise) + */ int -find_xml_children(xmlNode ** children, xmlNode * root, - const char *tag, const char *field, const char *value, gboolean search_matches) +pcmk__xe_delete_match(xmlNode *xml, xmlNode *search) { - int match_found = 0; + // See @COMPAT comment in pcmk__xe_replace_match() + CRM_CHECK((xml != NULL) && (search != NULL), return EINVAL); - CRM_CHECK(root != NULL, return FALSE); - CRM_CHECK(children != NULL, return FALSE); - - if ((tag != NULL) && !pcmk__xe_is(root, tag)) { + for (xml = pcmk__xe_first_child(xml, NULL, NULL, NULL); xml != NULL; + xml = pcmk__xe_next(xml)) { - } else if (value != NULL && !pcmk__str_eq(value, crm_element_value(root, field), pcmk__str_casei)) { - - } else { - if (*children == NULL) { - *children = create_xml_node(NULL, __func__); + if (!pcmk__xml_tree_foreach(xml, delete_xe_if_matching, search)) { + // Found and deleted an element + return pcmk_rc_ok; } - add_node_copy(*children, root); - match_found = 1; } - if (search_matches || match_found == 0) { - xmlNode *child = NULL; + // No match found in this subtree + return ENXIO; +} - for (child = pcmk__xml_first_child(root); child != NULL; - child = pcmk__xml_next(child)) { - match_found += find_xml_children(children, child, tag, field, value, search_matches); - } - } +/*! + * \internal + * \brief Replace one XML node with a copy of another XML node + * + * This function handles change tracking and applies ACLs. + * + * \param[in,out] old XML node to replace + * \param[in] new XML node to copy as replacement for \p old + * + * \note This frees \p old. + */ +static void +replace_node(xmlNode *old, xmlNode *new) +{ + new = xmlCopyNode(new, 1); + pcmk__mem_assert(new); - return match_found; + // May be unnecessary but avoids slight changes to some test outputs + pcmk__xml_tree_foreach(new, reset_xml_node_flags, NULL); + + old = xmlReplaceNode(old, new); + + if (xml_tracking_changes(new)) { + // Replaced sections may have included relevant ACLs + pcmk__apply_acl(new); + } + xml_calculate_changes(old, new); + xmlFreeNode(old); } -gboolean -replace_xml_child(xmlNode * parent, xmlNode * child, xmlNode * update, gboolean delete_only) +/*! + * \internal + * \brief Replace one XML subtree with a copy of another if the two match + * + * A match is defined as follows: + * * \p xml and \p user_data are both element nodes of the same type. + * * If \p user_data has the \c PCMK_XA_ID attribute set, then \p xml has + * \c PCMK_XA_ID set to the same value. + * + * \param[in,out] xml XML subtree to replace with \p user_data upon match + * \param[in] user_data XML to replace \p xml with a copy of upon match + * + * \return \c true to continue traversing the tree, or \c false to stop (because + * \p xml was replaced by \p user_data) + * + * \note This is compatible with \c pcmk__xml_tree_foreach(). + */ +static bool +replace_xe_if_matching(xmlNode *xml, void *user_data) { - gboolean can_delete = FALSE; - xmlNode *child_of_child = NULL; + xmlNode *replace = user_data; + const char *xml_id = NULL; + const char *replace_id = NULL; - const char *up_id = NULL; - const char *child_id = NULL; - const char *right_val = NULL; + xml_id = pcmk__xe_id(xml); + replace_id = pcmk__xe_id(replace); - CRM_CHECK(child != NULL, return FALSE); - CRM_CHECK(update != NULL, return FALSE); + if (!pcmk__xe_is(replace, (const char *) xml->name)) { + // No match: either not both elements, or different element types + return true; + } - up_id = ID(update); - child_id = ID(child); + if ((replace_id != NULL) + && !pcmk__str_eq(replace_id, xml_id, pcmk__str_none)) { - if (up_id == NULL || (child_id && strcmp(child_id, up_id) == 0)) { - can_delete = TRUE; + // No match: ID was provided in replace and doesn't match xml's ID + return true; } - if (!pcmk__xe_is(update, (const char *) child->name)) { - can_delete = FALSE; - } - if (can_delete && delete_only) { - for (xmlAttrPtr a = pcmk__xe_first_attr(update); a != NULL; - a = a->next) { - const char *p_name = (const char *) a->name; - const char *p_value = pcmk__xml_attr_value(a); - right_val = crm_element_value(child, p_name); - if (!pcmk__str_eq(p_value, right_val, pcmk__str_casei)) { - can_delete = FALSE; - } + crm_log_xml_trace(xml, "replace-match"); + crm_log_xml_trace(replace, "replace-with"); + replace_node(xml, replace); + + // Found a match and replaced it; stop traversing tree + return false; +} + +/*! + * \internal + * \brief Search an XML tree depth-first and replace the first matching element + * + * This function does not attempt to match the tree root (\p xml). + * + * A match with a node \c node is defined as follows: + * * \c node and \p replace are both element nodes of the same type. + * * If \p replace has the \c PCMK_XA_ID attribute set, then \c node has + * \c PCMK_XA_ID set to the same value. + * + * \param[in,out] xml XML tree to search + * \param[in] replace XML to replace a matching element with a copy of + * + * \return Standard Pacemaker return code (specifically, \c pcmk_rc_ok on + * successful replacement and an error code otherwise) + */ +int +pcmk__xe_replace_match(xmlNode *xml, xmlNode *replace) +{ + /* @COMPAT Some of this behavior (like not matching the tree root, which is + * allowed by pcmk__xe_update_match()) is questionable for general use but + * required for backward compatibility by cib_process_replace() and + * cib_process_delete(). Behavior can change at a major version release if + * desired. + */ + CRM_CHECK((xml != NULL) && (replace != NULL), return EINVAL); + + for (xml = pcmk__xe_first_child(xml, NULL, NULL, NULL); xml != NULL; + xml = pcmk__xe_next(xml)) { + + if (!pcmk__xml_tree_foreach(xml, replace_xe_if_matching, replace)) { + // Found and replaced an element + return pcmk_rc_ok; } } - if (can_delete && parent != NULL) { - crm_log_xml_trace(child, "Delete match found..."); - if (delete_only || update == NULL) { - free_xml(child); + // No match found in this subtree + return ENXIO; +} - } else { - xmlNode *old = child; - xmlNode *new = xmlCopyNode(update, 1); +//! User data for \c update_xe_if_matching() +struct update_data { + xmlNode *update; //!< Update source + uint32_t flags; //!< Group of <tt>enum pcmk__xa_flags</tt> +}; - CRM_ASSERT(new != NULL); +/*! + * \internal + * \brief Update one XML subtree with another if the two match + * + * "Update" means to merge a source subtree into a target subtree (see + * \c pcmk__xml_update()). + * + * A match is defined as follows: + * * \p xml and \p user_data->update are both element nodes of the same type. + * * \p xml and \p user_data->update have the same \c PCMK_XA_ID attribute + * value, or \c PCMK_XA_ID is unset in both + * + * \param[in,out] xml XML subtree to update with \p user_data->update + * upon match + * \param[in] user_data <tt>struct update_data</tt> object + * + * \return \c true to continue traversing the tree, or \c false to stop (because + * \p xml was updated by \p user_data->update) + * + * \note This is compatible with \c pcmk__xml_tree_foreach(). + */ +static bool +update_xe_if_matching(xmlNode *xml, void *user_data) +{ + struct update_data *data = user_data; + xmlNode *update = data->update; - // May be unnecessary but avoids slight changes to some test outputs - reset_xml_node_flags(new); + if (!pcmk__xe_is(update, (const char *) xml->name)) { + // No match: either not both elements, or different element types + return true; + } - old = xmlReplaceNode(old, new); + if (!pcmk__str_eq(pcmk__xe_id(xml), pcmk__xe_id(update), pcmk__str_none)) { + // No match: ID mismatch + return true; + } - if (xml_tracking_changes(new)) { - // Replaced sections may have included relevant ACLs - pcmk__apply_acl(new); - } - xml_calculate_changes(old, new); - xmlFreeNode(old); - } - return TRUE; + crm_log_xml_trace(xml, "update-match"); + crm_log_xml_trace(update, "update-with"); + pcmk__xml_update(NULL, xml, update, data->flags, false); - } else if (can_delete) { - crm_log_xml_debug(child, "Cannot delete the search root"); - can_delete = FALSE; - } + // Found a match and replaced it; stop traversing tree + return false; +} - child_of_child = pcmk__xml_first_child(child); - while (child_of_child) { - xmlNode *next = pcmk__xml_next(child_of_child); +/*! + * \internal + * \brief Search an XML tree depth-first and update the first matching element + * + * "Update" means to merge a source subtree into a target subtree (see + * \c pcmk__xml_update()). + * + * A match with a node \c node is defined as follows: + * * \c node and \p update are both element nodes of the same type. + * * \c node and \p update have the same \c PCMK_XA_ID attribute value, or + * \c PCMK_XA_ID is unset in both + * + * \param[in,out] xml XML tree to search + * \param[in] update XML to update a matching element with + * \param[in] flags Group of <tt>enum pcmk__xa_flags</tt> + * + * \return Standard Pacemaker return code (specifically, \c pcmk_rc_ok on + * successful update and an error code otherwise) + */ +int +pcmk__xe_update_match(xmlNode *xml, xmlNode *update, uint32_t flags) +{ + /* @COMPAT In pcmk__xe_delete_match() and pcmk__xe_replace_match(), we + * compare IDs only if the equivalent of the update argument has an ID. + * Here, we're stricter: we consider it a mismatch if only one element has + * an ID attribute, or if both elements have IDs but they don't match. + * + * Perhaps we should align the behavior at a major version release. + */ + struct update_data data = { + .update = update, + .flags = flags, + }; - can_delete = replace_xml_child(child, child_of_child, update, delete_only); + CRM_CHECK((xml != NULL) && (update != NULL), return EINVAL); - /* only delete the first one */ - if (can_delete) { - child_of_child = NULL; - } else { - child_of_child = next; - } + if (!pcmk__xml_tree_foreach(xml, update_xe_if_matching, &data)) { + // Found and updated an element + return pcmk_rc_ok; } - return can_delete; + // No match found in this subtree + return ENXIO; } xmlNode * @@ -2461,61 +2077,42 @@ sorted_xml(xmlNode *input, xmlNode *parent, gboolean recursive) CRM_CHECK(input != NULL, return NULL); - result = create_xml_node(parent, (const char *) input->name); + result = pcmk__xe_create(parent, (const char *) input->name); nvpairs = pcmk_xml_attrs2nvpairs(input); nvpairs = pcmk_sort_nvpairs(nvpairs); pcmk_nvpairs2xml_attrs(nvpairs, result); pcmk_free_nvpairs(nvpairs); - for (child = pcmk__xml_first_child(input); child != NULL; - child = pcmk__xml_next(child)) { + for (child = pcmk__xe_first_child(input, NULL, NULL, NULL); child != NULL; + child = pcmk__xe_next(child)) { if (recursive) { sorted_xml(child, result, recursive); } else { - add_node_copy(result, child); + pcmk__xml_copy(result, child); } } return result; } -xmlNode * -first_named_child(const xmlNode *parent, const char *name) -{ - xmlNode *match = NULL; - - for (match = pcmk__xe_first_child(parent); match != NULL; - match = pcmk__xe_next(match)) { - /* - * name == NULL gives first child regardless of name; this is - * semantically incorrect in this function, but may be necessary - * due to prior use of xml_child_iter_filter - */ - if (pcmk__str_eq(name, (const char *)match->name, pcmk__str_null_matches)) { - return match; - } - } - return NULL; -} - /*! - * \brief Get next instance of same XML tag + * \internal + * \brief Get next sibling XML element with the same name as a given element * - * \param[in] sibling XML tag to start from + * \param[in] node XML element to start from * - * \return Next sibling XML tag with same name + * \return Next sibling XML element with same name */ xmlNode * -crm_next_same_xml(const xmlNode *sibling) +pcmk__xe_next_same(const xmlNode *node) { - xmlNode *match = pcmk__xe_next(sibling); + for (xmlNode *match = pcmk__xe_next(node); match != NULL; + match = pcmk__xe_next(match)) { - while (match != NULL) { - if (pcmk__xe_is(match, (const char *) sibling->name)) { + if (pcmk__xe_is(match, (const char *) node->name)) { return match; } - match = pcmk__xe_next(match); } return NULL; } @@ -2554,31 +2151,32 @@ crm_xml_cleanup(void) xmlNode * expand_idref(xmlNode * input, xmlNode * top) { + char *xpath = NULL; const char *ref = NULL; - xmlNode *result = input; + xmlNode *result = NULL; - if (result == NULL) { + if (input == NULL) { return NULL; - - } else if (top == NULL) { - top = input; } - ref = crm_element_value(result, XML_ATTR_IDREF); - if (ref != NULL) { - char *xpath_string = crm_strdup_printf("//%s[@" XML_ATTR_ID "='%s']", - result->name, ref); + ref = crm_element_value(input, PCMK_XA_ID_REF); + if (ref == NULL) { + return input; + } - result = get_xpath_object(xpath_string, top, LOG_ERR); - if (result == NULL) { - char *nodePath = (char *)xmlGetNodePath(top); + if (top == NULL) { + top = input; + } - crm_err("No match for %s found in %s: Invalid configuration", - xpath_string, pcmk__s(nodePath, "unrecognizable path")); - free(nodePath); - } - free(xpath_string); + xpath = crm_strdup_printf("//%s[@" PCMK_XA_ID "='%s']", input->name, ref); + result = get_xpath_object(xpath, top, LOG_DEBUG); + if (result == NULL) { // Not possible with schema validation enabled + pcmk__config_err("Ignoring invalid %s configuration: " + PCMK_XA_ID_REF " '%s' does not reference " + "a valid object " CRM_XS " xpath=%s", + input->name, ref, xpath); } + free(xpath); return result; } @@ -2610,25 +2208,52 @@ pcmk__xml_artefact_root(enum pcmk__xml_artefact_ns ns) return ret; } -char * -pcmk__xml_artefact_path(enum pcmk__xml_artefact_ns ns, const char *filespec) +static char * +find_artefact(enum pcmk__xml_artefact_ns ns, const char *path, const char *filespec) { - char *base = pcmk__xml_artefact_root(ns), *ret = NULL; + char *ret = NULL; switch (ns) { case pcmk__xml_artefact_ns_legacy_rng: case pcmk__xml_artefact_ns_base_rng: - ret = crm_strdup_printf("%s/%s.rng", base, filespec); + if (pcmk__ends_with(filespec, ".rng")) { + ret = crm_strdup_printf("%s/%s", path, filespec); + } else { + ret = crm_strdup_printf("%s/%s.rng", path, filespec); + } break; case pcmk__xml_artefact_ns_legacy_xslt: case pcmk__xml_artefact_ns_base_xslt: - ret = crm_strdup_printf("%s/%s.xsl", base, filespec); + if (pcmk__ends_with(filespec, ".xsl")) { + ret = crm_strdup_printf("%s/%s", path, filespec); + } else { + ret = crm_strdup_printf("%s/%s.xsl", path, filespec); + } break; default: crm_err("XML artefact family specified as %u not recognized", ns); } + + return ret; +} + +char * +pcmk__xml_artefact_path(enum pcmk__xml_artefact_ns ns, const char *filespec) +{ + struct stat sb; + char *base = pcmk__xml_artefact_root(ns); + char *ret = NULL; + + ret = find_artefact(ns, base, filespec); free(base); + if (stat(ret, &sb) != 0 || !S_ISREG(sb.st_mode)) { + const char *remote_schema_dir = pcmk__remote_schema_dir(); + + free(ret); + ret = find_artefact(ns, remote_schema_dir, filespec); + } + return ret; } @@ -2669,8 +2294,9 @@ pcmk__xe_foreach_child(xmlNode *xml, const char *child_element_name, CRM_ASSERT(handler != NULL); for (xmlNode *node = children; node != NULL; node = node->next) { - if (node->type == XML_ELEMENT_NODE && - pcmk__str_eq(child_element_name, (const char *) node->name, pcmk__str_null_matches)) { + if ((node->type == XML_ELEMENT_NODE) + && ((child_element_name == NULL) + || pcmk__xe_is(node, child_element_name))) { int rc = handler(node, userdata); if (rc != pcmk_rc_ok) { @@ -2690,8 +2316,8 @@ pcmk__xe_foreach_child(xmlNode *xml, const char *child_element_name, xmlNode * find_entity(xmlNode *parent, const char *node_name, const char *id) { - return pcmk__xe_match(parent, node_name, - ((id == NULL)? id : XML_ATTR_ID), id); + return pcmk__xe_first_child(parent, node_name, + ((id == NULL)? id : PCMK_XA_ID), id); } void @@ -2709,12 +2335,28 @@ getDocPtr(xmlNode *node) doc = node->doc; if (doc == NULL) { - doc = xmlNewDoc((pcmkXmlStr) "1.0"); + doc = xmlNewDoc(PCMK__XML_VERSION); xmlDocSetRootElement(doc, node); } return doc; } +xmlNode * +add_node_copy(xmlNode *parent, xmlNode *src_node) +{ + xmlNode *child = NULL; + + CRM_CHECK((parent != NULL) && (src_node != NULL), return NULL); + + child = xmlDocCopyNode(src_node, parent->doc, 1); + if (child == NULL) { + return NULL; + } + xmlAddChild(parent, child); + pcmk__xml_mark_created(child); + return child; +} + int add_node_nocopy(xmlNode *parent, const char *name, xmlNode *child) { @@ -2732,5 +2374,352 @@ xml_has_children(const xmlNode * xml_root) return FALSE; } +static char * +replace_text(char *text, size_t *index, size_t *length, const char *replace) +{ + // We have space for 1 char already + size_t offset = strlen(replace) - 1; + + if (offset > 0) { + *length += offset; + text = pcmk__realloc(text, *length + 1); + + // Shift characters to the right to make room for the replacement string + for (size_t i = *length; i > (*index + offset); i--) { + text[i] = text[i - offset]; + } + } + + // Replace the character at index by the replacement string + memcpy(text + *index, replace, offset + 1); + + // Reset index to the end of replacement string + *index += offset; + return text; +} + +char * +crm_xml_escape(const char *text) +{ + size_t length = 0; + char *copy = NULL; + + if (text == NULL) { + return NULL; + } + + length = strlen(text); + copy = pcmk__str_copy(text); + for (size_t index = 0; index <= length; index++) { + if(copy[index] & 0x80 && copy[index+1] & 0x80){ + index++; + continue; + } + switch (copy[index]) { + case 0: + // Sanity only; loop should stop at the last non-null byte + break; + case '<': + copy = replace_text(copy, &index, &length, "<"); + break; + case '>': + copy = replace_text(copy, &index, &length, ">"); + break; + case '"': + copy = replace_text(copy, &index, &length, """); + break; + case '\'': + copy = replace_text(copy, &index, &length, "'"); + break; + case '&': + copy = replace_text(copy, &index, &length, "&"); + break; + case '\t': + /* Might as well just expand to a few spaces... */ + copy = replace_text(copy, &index, &length, " "); + break; + case '\n': + copy = replace_text(copy, &index, &length, "\\n"); + break; + case '\r': + copy = replace_text(copy, &index, &length, "\\r"); + break; + default: + /* Check for and replace non-printing characters with their octal equivalent */ + if(copy[index] < ' ' || copy[index] > '~') { + char *replace = crm_strdup_printf("\\%.3o", copy[index]); + + copy = replace_text(copy, &index, &length, replace); + free(replace); + } + } + } + return copy; +} + +xmlNode * +copy_xml(xmlNode *src) +{ + xmlDoc *doc = xmlNewDoc(PCMK__XML_VERSION); + xmlNode *copy = NULL; + + pcmk__mem_assert(doc); + + copy = xmlDocCopyNode(src, doc, 1); + pcmk__mem_assert(copy); + + xmlDocSetRootElement(doc, copy); + return copy; +} + +xmlNode * +create_xml_node(xmlNode *parent, const char *name) +{ + // Like pcmk__xe_create(), but returns NULL on failure + xmlNode *node = NULL; + + CRM_CHECK(!pcmk__str_empty(name), return NULL); + + if (parent == NULL) { + xmlDoc *doc = xmlNewDoc(PCMK__XML_VERSION); + + if (doc == NULL) { + return NULL; + } + + node = xmlNewDocRawNode(doc, NULL, (pcmkXmlStr) name, NULL); + if (node == NULL) { + xmlFreeDoc(doc); + return NULL; + } + xmlDocSetRootElement(doc, node); + + } else { + node = xmlNewChild(parent, NULL, (pcmkXmlStr) name, NULL); + if (node == NULL) { + return NULL; + } + } + pcmk__xml_mark_created(node); + return node; +} + +xmlNode * +pcmk_create_xml_text_node(xmlNode *parent, const char *name, + const char *content) +{ + xmlNode *node = pcmk__xe_create(parent, name); + + pcmk__xe_set_content(node, "%s", content); + return node; +} + +xmlNode * +pcmk_create_html_node(xmlNode *parent, const char *element_name, const char *id, + const char *class_name, const char *text) +{ + xmlNode *node = pcmk__html_create(parent, element_name, id, class_name); + + pcmk__xe_set_content(node, "%s", text); + return node; +} + +xmlNode * +first_named_child(const xmlNode *parent, const char *name) +{ + return pcmk__xe_first_child(parent, name, NULL, NULL); +} + +xmlNode * +find_xml_node(const xmlNode *root, const char *search_path, gboolean must_find) +{ + xmlNode *result = NULL; + + if (search_path == NULL) { + crm_warn("Will never find <NULL>"); + return NULL; + } + + result = pcmk__xe_first_child(root, search_path, NULL, NULL); + + if (must_find && (result == NULL)) { + crm_warn("Could not find %s in %s", + search_path, + ((root != NULL)? (const char *) root->name : "<NULL>")); + } + + return result; +} + +xmlNode * +crm_next_same_xml(const xmlNode *sibling) +{ + return pcmk__xe_next_same(sibling); +} + +void +xml_remove_prop(xmlNode * obj, const char *name) +{ + pcmk__xe_remove_attr(obj, name); +} + +gboolean +replace_xml_child(xmlNode * parent, xmlNode * child, xmlNode * update, gboolean delete_only) +{ + bool is_match = false; + const char *child_id = NULL; + const char *update_id = NULL; + + CRM_CHECK(child != NULL, return FALSE); + CRM_CHECK(update != NULL, return FALSE); + + child_id = pcmk__xe_id(child); + update_id = pcmk__xe_id(update); + + /* Match element name and (if provided in update XML) element ID. Don't + * match search root (child is search root if parent == NULL). + */ + is_match = (parent != NULL) + && pcmk__xe_is(update, (const char *) child->name) + && ((update_id == NULL) + || pcmk__str_eq(update_id, child_id, pcmk__str_none)); + + /* For deletion, match all attributes provided in update. A matching node + * can have additional attributes, but values must match for provided ones. + */ + if (is_match && delete_only) { + for (xmlAttr *attr = pcmk__xe_first_attr(update); attr != NULL; + attr = attr->next) { + const char *name = (const char *) attr->name; + const char *update_val = pcmk__xml_attr_value(attr); + const char *child_val = crm_element_value(child, name); + + if (!pcmk__str_eq(update_val, child_val, pcmk__str_casei)) { + is_match = false; + break; + } + } + } + + if (is_match) { + if (delete_only) { + crm_log_xml_trace(child, "delete-match"); + crm_log_xml_trace(update, "delete-search"); + free_xml(child); + + } else { + crm_log_xml_trace(child, "replace-match"); + crm_log_xml_trace(update, "replace-with"); + replace_node(child, update); + } + return TRUE; + } + + // Current node not a match; search the rest of the subtree depth-first + parent = child; + for (child = pcmk__xml_first_child(parent); child != NULL; + child = pcmk__xml_next(child)) { + + // Only delete/replace the first match + if (replace_xml_child(parent, child, update, delete_only)) { + return TRUE; + } + } + + // No match found in this subtree + return FALSE; +} + +gboolean +update_xml_child(xmlNode *child, xmlNode *to_update) +{ + return pcmk__xe_update_match(child, to_update, + pcmk__xaf_score_update) == pcmk_rc_ok; +} + +int +find_xml_children(xmlNode **children, xmlNode *root, const char *tag, + const char *field, const char *value, gboolean search_matches) +{ + int match_found = 0; + + CRM_CHECK(root != NULL, return FALSE); + CRM_CHECK(children != NULL, return FALSE); + + if ((tag != NULL) && !pcmk__xe_is(root, tag)) { + + } else if ((value != NULL) + && !pcmk__str_eq(value, crm_element_value(root, field), + pcmk__str_casei)) { + + } else { + if (*children == NULL) { + *children = pcmk__xe_create(NULL, __func__); + } + pcmk__xml_copy(*children, root); + match_found = 1; + } + + if (search_matches || match_found == 0) { + xmlNode *child = NULL; + + for (child = pcmk__xml_first_child(root); child != NULL; + child = pcmk__xml_next(child)) { + match_found += find_xml_children(children, child, tag, field, value, + search_matches); + } + } + + return match_found; +} + +void +fix_plus_plus_recursive(xmlNode *target) +{ + /* TODO: Remove recursion and use xpath searches for value++ */ + xmlNode *child = NULL; + + for (xmlAttrPtr a = pcmk__xe_first_attr(target); a != NULL; a = a->next) { + const char *p_name = (const char *) a->name; + const char *p_value = pcmk__xml_attr_value(a); + + expand_plus_plus(target, p_name, p_value); + } + for (child = pcmk__xe_first_child(target, NULL, NULL, NULL); child != NULL; + child = pcmk__xe_next(child)) { + + fix_plus_plus_recursive(child); + } +} + +void +copy_in_properties(xmlNode *target, const xmlNode *src) +{ + if (src == NULL) { + crm_warn("No node to copy properties from"); + + } else if (target == NULL) { + crm_err("No node to copy properties into"); + + } else { + for (xmlAttrPtr a = pcmk__xe_first_attr(src); a != NULL; a = a->next) { + const char *p_name = (const char *) a->name; + const char *p_value = pcmk__xml_attr_value(a); + + expand_plus_plus(target, p_name, p_value); + if (xml_acl_denied(target)) { + crm_trace("Cannot copy %s=%s to %s", p_name, p_value, target->name); + return; + } + } + } +} + +void +expand_plus_plus(xmlNode * target, const char *name, const char *value) +{ + pcmk__xe_set_score(target, name, value); +} + // LCOV_EXCL_STOP // End deprecated API |