Page Menu
Home
ClusterLabs Projects
Search
Configure Global Search
Log In
Files
F1842342
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Flag For Later
Award Token
Size
126 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/include/crm/common/xml_internal.h b/include/crm/common/xml_internal.h
index 177aa09bed..ccabd77b7a 100644
--- a/include/crm/common/xml_internal.h
+++ b/include/crm/common/xml_internal.h
@@ -1,612 +1,613 @@
/*
* Copyright 2017-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
* This source code is licensed under the GNU Lesser General Public License
* version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
*/
#ifndef PCMK__CRM_COMMON_XML_INTERNAL__H
#define PCMK__CRM_COMMON_XML_INTERNAL__H
/*
* Internal-only wrappers for and extensions to libxml2 (libxslt)
*/
#include <stdlib.h>
#include <stdint.h> // uint32_t
#include <stdio.h>
#include <string.h>
#include <crm/crm.h> /* transitively imports qblog.h */
#include <crm/common/output_internal.h>
#include <crm/common/xml_idref_internal.h>
#include <crm/common/xml_io_internal.h>
#include <crm/common/xml_names_internal.h> // PCMK__XE_PROMOTABLE_LEGACY
#include <crm/common/xml_names.h> // PCMK_XA_ID, PCMK_XE_CLONE
#include <libxml/relaxng.h>
#ifdef __cplusplus
extern "C" {
#endif
/*!
* \brief Base for directing lib{xml2,xslt} log into standard libqb backend
*
* This macro implements the core of what can be needed for directing
* libxml2 or libxslt error messaging into standard, preconfigured
* libqb-backed log stream.
*
* It's a bit unfortunate that libxml2 (and more sparsely, also libxslt)
* emits a single message by chunks (location is emitted separatedly from
* the message itself), so we have to take the effort to combine these
* chunks back to single message. Whether to do this or not is driven
* with \p dechunk toggle.
*
* The form of a macro was chosen for implicit deriving of __FILE__, etc.
* and also because static dechunking buffer should be differentiated per
* library (here we assume different functions referring to this macro
* will not ever be using both at once), preferably also per-library
* context of use to avoid clashes altogether.
*
* Note that we cannot use qb_logt, because callsite data have to be known
* at the moment of compilation, which it is not always the case -- xml_log
* (and unfortunately there's no clear explanation of the fail to compile).
*
* Also note that there's no explicit guard against said libraries producing
* never-newline-terminated chunks (which would just keep consuming memory),
* as it's quite improbable. Termination of the program in between the
* same-message chunks will raise a flag with valgrind and the likes, though.
*
* And lastly, regarding how dechunking combines with other non-message
* parameters -- for \p priority, most important running specification
* wins (possibly elevated to LOG_ERR in case of nonconformance with the
* newline-termination "protocol"), \p dechunk is expected to always be
* on once it was at the start, and the rest (\p postemit and \p prefix)
* are picked directly from the last chunk entry finalizing the message
* (also reasonable to always have it the same with all related entries).
*
* \param[in] priority Syslog priority for the message to be logged
* \param[in] dechunk Whether to dechunk new-line terminated message
* \param[in] postemit Code to be executed once message is sent out
* \param[in] prefix How to prefix the message or NULL for raw passing
* \param[in] fmt Format string as with printf-like functions
* \param[in] ap Variable argument list to supplement \p fmt format string
*/
#define PCMK__XML_LOG_BASE(priority, dechunk, postemit, prefix, fmt, ap) \
do { \
if (!(dechunk) && (prefix) == NULL) { /* quick pass */ \
qb_log_from_external_source_va(__func__, __FILE__, (fmt), \
(priority), __LINE__, 0, (ap)); \
(void) (postemit); \
} else { \
int CXLB_len = 0; \
char *CXLB_buf = NULL; \
static int CXLB_buffer_len = 0; \
static char *CXLB_buffer = NULL; \
static uint8_t CXLB_priority = 0; \
\
CXLB_len = vasprintf(&CXLB_buf, (fmt), (ap)); \
\
if (CXLB_len <= 0 || CXLB_buf[CXLB_len - 1] == '\n' || !(dechunk)) { \
if (CXLB_len < 0) { \
CXLB_buf = (char *) "LOG CORRUPTION HAZARD"; /*we don't modify*/\
CXLB_priority = QB_MIN(CXLB_priority, LOG_ERR); \
} else if (CXLB_len > 0 /* && (dechunk) */ \
&& CXLB_buf[CXLB_len - 1] == '\n') { \
CXLB_buf[CXLB_len - 1] = '\0'; \
} \
if (CXLB_buffer) { \
qb_log_from_external_source(__func__, __FILE__, "%s%s%s", \
CXLB_priority, __LINE__, 0, \
(prefix) != NULL ? (prefix) : "", \
CXLB_buffer, CXLB_buf); \
free(CXLB_buffer); \
} else { \
qb_log_from_external_source(__func__, __FILE__, "%s%s", \
(priority), __LINE__, 0, \
(prefix) != NULL ? (prefix) : "", \
CXLB_buf); \
} \
if (CXLB_len < 0) { \
CXLB_buf = NULL; /* restore temporary override */ \
} \
CXLB_buffer = NULL; \
CXLB_buffer_len = 0; \
(void) (postemit); \
\
} else if (CXLB_buffer == NULL) { \
CXLB_buffer_len = CXLB_len; \
CXLB_buffer = CXLB_buf; \
CXLB_buf = NULL; \
CXLB_priority = (priority); /* remember as a running severest */ \
\
} else { \
CXLB_buffer = realloc(CXLB_buffer, 1 + CXLB_buffer_len + CXLB_len); \
memcpy(CXLB_buffer + CXLB_buffer_len, CXLB_buf, CXLB_len); \
CXLB_buffer_len += CXLB_len; \
CXLB_buffer[CXLB_buffer_len] = '\0'; \
CXLB_priority = QB_MIN(CXLB_priority, (priority)); /* severest? */ \
} \
free(CXLB_buf); \
} \
} while (0)
/*
* \enum pcmk__xml_fmt_options
* \brief Bit flags to control format in XML logs and dumps
*/
enum pcmk__xml_fmt_options {
//! Exclude certain XML attributes (for calculating digests)
pcmk__xml_fmt_filtered = (1 << 0),
//! Include indentation and newlines
pcmk__xml_fmt_pretty = (1 << 1),
//! Include the opening tag of an XML element, and include XML comments
pcmk__xml_fmt_open = (1 << 3),
//! Include the children of an XML element
pcmk__xml_fmt_children = (1 << 4),
//! Include the closing tag of an XML element
pcmk__xml_fmt_close = (1 << 5),
// @COMPAT Can we start including text nodes unconditionally?
//! Include XML text nodes
pcmk__xml_fmt_text = (1 << 6),
};
void pcmk__xml_init(void);
void pcmk__xml_cleanup(void);
int pcmk__xml_show(pcmk__output_t *out, const char *prefix, const xmlNode *data,
int depth, uint32_t options);
int pcmk__xml_show_changes(pcmk__output_t *out, const xmlNode *xml);
/* XML search strings for guest, remote and pacemaker_remote nodes */
/* search string to find CIB resources entries for cluster nodes */
#define PCMK__XP_MEMBER_NODE_CONFIG \
"//" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION "/" PCMK_XE_NODES \
"/" PCMK_XE_NODE \
"[not(@" PCMK_XA_TYPE ") or @" PCMK_XA_TYPE "='" PCMK_VALUE_MEMBER "']"
/* search string to find CIB resources entries for guest nodes */
#define PCMK__XP_GUEST_NODE_CONFIG \
"//" PCMK_XE_CIB "//" PCMK_XE_CONFIGURATION "//" PCMK_XE_PRIMITIVE \
"//" PCMK_XE_META_ATTRIBUTES "//" PCMK_XE_NVPAIR \
"[@" PCMK_XA_NAME "='" PCMK_META_REMOTE_NODE "']"
/* search string to find CIB resources entries for remote nodes */
#define PCMK__XP_REMOTE_NODE_CONFIG \
"//" PCMK_XE_CIB "//" PCMK_XE_CONFIGURATION "//" PCMK_XE_PRIMITIVE \
"[@" PCMK_XA_TYPE "='" PCMK_VALUE_REMOTE "']" \
"[@" PCMK_XA_PROVIDER "='pacemaker']"
/* search string to find CIB node status entries for pacemaker_remote nodes */
#define PCMK__XP_REMOTE_NODE_STATUS \
"//" PCMK_XE_CIB "//" PCMK_XE_STATUS "//" PCMK__XE_NODE_STATE \
"[@" PCMK_XA_REMOTE_NODE "='" PCMK_VALUE_TRUE "']"
/*!
* \internal
* \brief Serialize XML (using libxml) into provided descriptor
*
* \param[in] fd File descriptor to (piece-wise) write to
* \param[in] cur XML subtree to proceed
*
* \return a standard Pacemaker return code
*/
int pcmk__xml2fd(int fd, xmlNode *cur);
enum pcmk__xml_artefact_ns {
pcmk__xml_artefact_ns_legacy_rng = 1,
pcmk__xml_artefact_ns_legacy_xslt,
pcmk__xml_artefact_ns_base_rng,
pcmk__xml_artefact_ns_base_xslt,
};
void pcmk__strip_xml_text(xmlNode *xml);
const char *pcmk__xe_add_last_written(xmlNode *xe);
xmlNode *pcmk__xe_first_child(const xmlNode *parent, const char *node_name,
const char *attr_n, const char *attr_v);
void pcmk__xe_remove_attr(xmlNode *element, const char *name);
bool pcmk__xe_remove_attr_cb(xmlNode *xml, void *user_data);
void pcmk__xe_remove_matching_attrs(xmlNode *element,
bool (*match)(xmlAttrPtr, void *),
void *user_data);
int pcmk__xe_delete_match(xmlNode *xml, xmlNode *search);
int pcmk__xe_replace_match(xmlNode *xml, xmlNode *replace);
int pcmk__xe_update_match(xmlNode *xml, xmlNode *update, uint32_t flags);
GString *pcmk__element_xpath(const xmlNode *xml);
/*!
* \internal
* \enum pcmk__xml_escape_type
* \brief Indicators of which XML characters to escape
*
* XML allows the escaping of special characters by replacing them with entity
* references (for example, <tt>"""</tt>) or character references (for
* example, <tt>" "</tt>).
*
* The special characters <tt>'&'</tt> (except as the beginning of an entity
* reference) and <tt>'<'</tt> are not allowed in their literal forms in XML
* character data. Character data is non-markup text (for example, the content
* of a text node). <tt>'>'</tt> is allowed under most circumstances; we escape
* it for safety and symmetry.
*
* For more details, see the "Character Data and Markup" section of the XML
* spec, currently section 2.4:
* https://www.w3.org/TR/xml/#dt-markup
*
* Attribute values are handled specially.
* * If an attribute value is delimited by single quotes, then single quotes
* must be escaped within the value.
* * Similarly, if an attribute value is delimited by double quotes, then double
* quotes must be escaped within the value.
* * A conformant XML processor replaces a literal whitespace character (tab,
* newline, carriage return, space) in an attribute value with a space
* (\c '#x20') character. However, a reference to a whitespace character (for
* example, \c "
" for \c '\n') does not get replaced.
* * For more details, see the "Attribute-Value Normalization" section of the
* XML spec, currently section 3.3.3. Note that the default attribute type
* is CDATA; we don't deal with NMTOKENS, etc.:
* https://www.w3.org/TR/xml/#AVNormalize
*
* Pacemaker always delimits attribute values with double quotes, so there's no
* need to escape single quotes.
*
* Newlines and tabs should be escaped in attribute values when XML is
* serialized to text, so that future parsing preserves them rather than
* normalizing them to spaces.
*
* We always escape carriage returns, so that they're not converted to spaces
* during attribute-value normalization and because displaying them as literals
* is messy.
*/
enum pcmk__xml_escape_type {
/*!
* For text nodes.
* * Escape \c '<', \c '>', and \c '&' using entity references.
* * Do not escape \c '\n' and \c '\t'.
* * Escape other non-printing characters using character references.
*/
pcmk__xml_escape_text,
/*!
* For attribute values.
* * Escape \c '<', \c '>', \c '&', and \c '"' using entity references.
* * Escape \c '\n', \c '\t', and other non-printing characters using
* character references.
*/
pcmk__xml_escape_attr,
/* @COMPAT Drop escaping of at least '\n' and '\t' for
* pcmk__xml_escape_attr_pretty when openstack-info, openstack-floating-ip,
* and openstack-virtual-ip resource agents no longer depend on it.
*
* At time of writing, openstack-info may set a multiline value for the
* openstack_ports node attribute. The other two agents query the value and
* require it to be on one line with no spaces.
*/
/*!
* For attribute values displayed in text output delimited by double quotes.
* * Escape \c '\n' as \c "\\n"
* * Escape \c '\r' as \c "\\r"
* * Escape \c '\t' as \c "\\t"
* * Escape \c '"' as \c "\\""
*/
pcmk__xml_escape_attr_pretty,
};
bool pcmk__xml_needs_escape(const char *text, enum pcmk__xml_escape_type type);
char *pcmk__xml_escape(const char *text, enum pcmk__xml_escape_type type);
/*!
* \internal
* \brief Get the root directory to scan XML artefacts of given kind for
*
* \param[in] ns governs the hierarchy nesting against the inherent root dir
*
* \return root directory to scan XML artefacts of given kind for
*/
char *
pcmk__xml_artefact_root(enum pcmk__xml_artefact_ns ns);
/*!
* \internal
* \brief Get the fully unwrapped path to particular XML artifact (RNG/XSLT)
*
* \param[in] ns denotes path forming details (parent dir, suffix)
* \param[in] filespec symbolic file specification to be combined with
* #artefact_ns to form the final path
* \return unwrapped path to particular XML artifact (RNG/XSLT)
*/
char *pcmk__xml_artefact_path(enum pcmk__xml_artefact_ns ns,
const char *filespec);
/*!
* \internal
* \brief Retrieve the value of the \c PCMK_XA_ID XML attribute
*
* \param[in] xml XML element to check
*
* \return Value of the \c PCMK_XA_ID attribute (may be \c NULL)
*/
static inline const char *
pcmk__xe_id(const xmlNode *xml)
{
return crm_element_value(xml, PCMK_XA_ID);
}
/*!
* \internal
* \brief Check whether an XML element is of a particular type
*
* \param[in] xml XML element to compare
* \param[in] name XML element name to compare
*
* \return \c true if \p xml is of type \p name, otherwise \c false
*/
static inline bool
pcmk__xe_is(const xmlNode *xml, const char *name)
{
return (xml != NULL) && (xml->name != NULL) && (name != NULL)
&& (strcmp((const char *) xml->name, name) == 0);
}
/*!
* \internal
* \brief Return first non-text child node of an XML node
*
* \param[in] parent XML node to check
*
* \return First non-text child node of \p parent (or NULL if none)
*/
static inline xmlNode *
pcmk__xml_first_child(const xmlNode *parent)
{
xmlNode *child = (parent? parent->children : NULL);
while (child && (child->type == XML_TEXT_NODE)) {
child = child->next;
}
return child;
}
/*!
* \internal
* \brief Return next non-text sibling node of an XML node
*
* \param[in] child XML node to check
*
* \return Next non-text sibling of \p child (or NULL if none)
*/
static inline xmlNode *
pcmk__xml_next(const xmlNode *child)
{
xmlNode *next = (child? child->next : NULL);
while (next && (next->type == XML_TEXT_NODE)) {
next = next->next;
}
return next;
}
/*!
* \internal
* \brief Return next non-text sibling element of an XML element
*
* \param[in] child XML element to check
*
* \return Next sibling element of \p child (or NULL if none)
*/
static inline xmlNode *
pcmk__xe_next(const xmlNode *child)
{
xmlNode *next = child? child->next : NULL;
while (next && (next->type != XML_ELEMENT_NODE)) {
next = next->next;
}
return next;
}
xmlNode *pcmk__xe_create(xmlNode *parent, const char *name);
void pcmk__xml_free(xmlNode *xml);
+void pcmk__xml_free_doc(xmlDoc *doc);
xmlNode *pcmk__xml_copy(xmlNode *parent, xmlNode *src);
xmlNode *pcmk__xe_next_same(const xmlNode *node);
void pcmk__xe_set_content(xmlNode *node, const char *format, ...)
G_GNUC_PRINTF(2, 3);
/*!
* \internal
* \enum pcmk__xa_flags
* \brief Flags for operations affecting XML attributes
*/
enum pcmk__xa_flags {
//! Flag has no effect
pcmk__xaf_none = 0U,
//! Don't overwrite existing values
pcmk__xaf_no_overwrite = (1U << 0),
/*!
* Treat values as score updates where possible (see
* \c pcmk__xe_set_score())
*/
pcmk__xaf_score_update = (1U << 1),
};
int pcmk__xe_copy_attrs(xmlNode *target, const xmlNode *src, uint32_t flags);
void pcmk__xe_sort_attrs(xmlNode *xml);
void pcmk__xml_sanitize_id(char *id);
void pcmk__xe_set_id(xmlNode *xml, const char *format, ...)
G_GNUC_PRINTF(2, 3);
/*!
* \internal
* \brief Like pcmk__xe_set_props, but takes a va_list instead of
* arguments directly.
*
* \param[in,out] node XML to add attributes to
* \param[in] pairs NULL-terminated list of name/value pairs to add
*/
void
pcmk__xe_set_propv(xmlNodePtr node, va_list pairs);
/*!
* \internal
* \brief Add a NULL-terminated list of name/value pairs to the given
* XML node as properties.
*
* \param[in,out] node XML node to add properties to
* \param[in] ... NULL-terminated list of name/value pairs
*
* \note A NULL name terminates the arguments; a NULL value will be skipped.
*/
void
pcmk__xe_set_props(xmlNodePtr node, ...)
G_GNUC_NULL_TERMINATED;
/*!
* \internal
* \brief Get first attribute of an XML element
*
* \param[in] xe XML element to check
*
* \return First attribute of \p xe (or NULL if \p xe is NULL or has none)
*/
static inline xmlAttr *
pcmk__xe_first_attr(const xmlNode *xe)
{
return (xe == NULL)? NULL : xe->properties;
}
/*!
* \internal
* \brief Extract the ID attribute from an XML element
*
* \param[in] xpath String to search
* \param[in] node Node to get the ID for
*
* \return ID attribute of \p node in xpath string \p xpath
*/
char *
pcmk__xpath_node_id(const char *xpath, const char *node);
/*!
* \internal
* \brief Print an informational message if an xpath query returned multiple
* items with the same ID.
*
* \param[in,out] out The output object
* \param[in] search The xpath search result, most typically the result of
* calling cib->cmds->query().
* \param[in] name The name searched for
*/
void
pcmk__warn_multiple_name_matches(pcmk__output_t *out, xmlNode *search,
const char *name);
/* internal XML-related utilities */
enum xml_private_flags {
pcmk__xf_none = 0x0000,
pcmk__xf_dirty = 0x0001,
pcmk__xf_deleted = 0x0002,
pcmk__xf_created = 0x0004,
pcmk__xf_modified = 0x0008,
pcmk__xf_tracking = 0x0010,
pcmk__xf_processed = 0x0020,
pcmk__xf_skip = 0x0040,
pcmk__xf_moved = 0x0080,
pcmk__xf_acl_enabled = 0x0100,
pcmk__xf_acl_read = 0x0200,
pcmk__xf_acl_write = 0x0400,
pcmk__xf_acl_deny = 0x0800,
pcmk__xf_acl_create = 0x1000,
pcmk__xf_acl_denied = 0x2000,
pcmk__xf_lazy = 0x4000,
};
void pcmk__set_xml_doc_flag(xmlNode *xml, enum xml_private_flags flag);
/*!
* \internal
* \brief Iterate over child elements of \p xml
*
* This function iterates over the children of \p xml, performing the
* callback function \p handler on each node. If the callback returns
* a value other than pcmk_rc_ok, the iteration stops and the value is
* returned. It is therefore possible that not all children will be
* visited.
*
* \param[in,out] xml The starting XML node. Can be NULL.
* \param[in] child_element_name The name that the node must match in order
* for \p handler to be run. If NULL, all
* child elements will match.
* \param[in] handler The callback function.
* \param[in,out] userdata User data to pass to the callback function.
* Can be NULL.
*
* \return Standard Pacemaker return code
*/
int
pcmk__xe_foreach_child(xmlNode *xml, const char *child_element_name,
int (*handler)(xmlNode *xml, void *userdata),
void *userdata);
bool pcmk__xml_tree_foreach(xmlNode *xml, bool (*fn)(xmlNode *, void *),
void *user_data);
static inline const char *
pcmk__xml_attr_value(const xmlAttr *attr)
{
return ((attr == NULL) || (attr->children == NULL))? NULL
: (const char *) attr->children->content;
}
// @COMPAT Drop when PCMK__XE_PROMOTABLE_LEGACY is removed
static inline const char *
pcmk__map_element_name(const xmlNode *xml)
{
if (xml == NULL) {
return NULL;
} else if (pcmk__xe_is(xml, PCMK__XE_PROMOTABLE_LEGACY)) {
return PCMK_XE_CLONE;
} else {
return (const char *) xml->name;
}
}
/*!
* \internal
* \brief Check whether a given CIB element was modified in a CIB patchset
*
* \param[in] patchset CIB XML patchset
* \param[in] element XML tag of CIB element to check (\c NULL is equivalent
* to \c PCMK_XE_CIB). Supported values include any CIB
* element supported by \c pcmk__cib_abs_xpath_for().
*
* \return \c true if \p element was modified, or \c false otherwise
*/
bool pcmk__cib_element_in_patchset(const xmlNode *patchset,
const char *element);
#ifdef __cplusplus
}
#endif
#endif // PCMK__CRM_COMMON_XML_INTERNAL__H
diff --git a/lib/common/tests/xml/pcmk__xml_init_test.c b/lib/common/tests/xml/pcmk__xml_init_test.c
index bfb34d98fc..13039720d9 100644
--- a/lib/common/tests/xml/pcmk__xml_init_test.c
+++ b/lib/common/tests/xml/pcmk__xml_init_test.c
@@ -1,195 +1,190 @@
/*
* Copyright 2023-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
* This source code is licensed under the GNU General Public License version 2
* or later (GPLv2+) WITHOUT ANY WARRANTY.
*/
#include <crm_internal.h>
#include <crm/common/xml.h>
#include <crm/common/unittest_internal.h>
#include <crm/common/xml_internal.h>
#include "crmcommon_private.h"
static void
buffer_scheme_test(void **state) {
assert_int_equal(XML_BUFFER_ALLOC_DOUBLEIT, xmlGetBufferAllocationScheme());
}
/* These functions also serve as unit tests of the static new_private_data
* function. We can't test free_private_data because libxml will call that as
* part of freeing everything else. By the time we'd get back into a unit test
* where we could check that private members are NULL, the structure containing
* the private data would have been freed.
*
* This could probably be tested with a lot of function mocking, but that
* doesn't seem worth it.
*/
static void
create_element_node(void **state) {
xml_doc_private_t *docpriv = NULL;
xml_node_private_t *priv = NULL;
xmlDoc *doc = pcmk__xml_new_doc();
xmlNodePtr node = xmlNewDocNode(doc, NULL, (pcmkXmlStr) "test", NULL);
/* Adding a node to the document marks it as dirty */
docpriv = doc->_private;
assert_true(pcmk_all_flags_set(docpriv->flags, pcmk__xf_dirty));
/* Double check things */
assert_non_null(node);
assert_int_equal(node->type, XML_ELEMENT_NODE);
/* Check that the private data is initialized correctly */
priv = node->_private;
assert_non_null(priv);
assert_int_equal(priv->check, PCMK__XML_NODE_PRIVATE_MAGIC);
assert_true(pcmk_all_flags_set(priv->flags, pcmk__xf_dirty|pcmk__xf_created));
/* Clean up */
- pcmk__xml_free(node);
- xmlFreeDoc(doc);
+ pcmk__xml_free_doc(doc);
}
static void
create_attr_node(void **state) {
xml_doc_private_t *docpriv = NULL;
xml_node_private_t *priv = NULL;
xmlDoc *doc = pcmk__xml_new_doc();
xmlNodePtr node = xmlNewDocNode(doc, NULL, (pcmkXmlStr) "test", NULL);
xmlAttrPtr attr = xmlNewProp(node, (pcmkXmlStr) PCMK_XA_NAME,
(pcmkXmlStr) "dummy-value");
/* Adding a node to the document marks it as dirty */
docpriv = doc->_private;
assert_true(pcmk_all_flags_set(docpriv->flags, pcmk__xf_dirty));
/* Double check things */
assert_non_null(attr);
assert_int_equal(attr->type, XML_ATTRIBUTE_NODE);
/* Check that the private data is initialized correctly */
priv = attr->_private;
assert_non_null(priv);
assert_int_equal(priv->check, PCMK__XML_NODE_PRIVATE_MAGIC);
assert_true(pcmk_all_flags_set(priv->flags, pcmk__xf_dirty|pcmk__xf_created));
/* Clean up */
- pcmk__xml_free(node);
- xmlFreeDoc(doc);
+ pcmk__xml_free_doc(doc);
}
static void
create_comment_node(void **state) {
xml_doc_private_t *docpriv = NULL;
xml_node_private_t *priv = NULL;
xmlDoc *doc = pcmk__xml_new_doc();
xmlNodePtr node = xmlNewDocComment(doc, (pcmkXmlStr) "blahblah");
/* Adding a node to the document marks it as dirty */
docpriv = doc->_private;
assert_true(pcmk_all_flags_set(docpriv->flags, pcmk__xf_dirty));
/* Double check things */
assert_non_null(node);
assert_int_equal(node->type, XML_COMMENT_NODE);
/* Check that the private data is initialized correctly */
priv = node->_private;
assert_non_null(priv);
assert_int_equal(priv->check, PCMK__XML_NODE_PRIVATE_MAGIC);
assert_true(pcmk_all_flags_set(priv->flags, pcmk__xf_dirty|pcmk__xf_created));
/* Clean up */
- pcmk__xml_free(node);
- xmlFreeDoc(doc);
+ pcmk__xml_free_doc(doc);
}
static void
create_text_node(void **state) {
xml_doc_private_t *docpriv = NULL;
xml_node_private_t *priv = NULL;
xmlDoc *doc = pcmk__xml_new_doc();
xmlNodePtr node = xmlNewDocText(doc, (pcmkXmlStr) "blahblah");
/* Adding a node to the document marks it as dirty */
docpriv = doc->_private;
assert_true(pcmk_all_flags_set(docpriv->flags, pcmk__xf_dirty));
/* Double check things */
assert_non_null(node);
assert_int_equal(node->type, XML_TEXT_NODE);
/* Check that no private data was created */
priv = node->_private;
assert_null(priv);
/* Clean up */
- pcmk__xml_free(node);
- xmlFreeDoc(doc);
+ pcmk__xml_free_doc(doc);
}
static void
create_dtd_node(void **state) {
xml_doc_private_t *docpriv = NULL;
xml_node_private_t *priv = NULL;
xmlDoc *doc = pcmk__xml_new_doc();
xmlDtdPtr dtd = xmlNewDtd(doc, (pcmkXmlStr) PCMK_XA_NAME,
(pcmkXmlStr) "externalId",
(pcmkXmlStr) "systemId");
/* Adding a node to the document marks it as dirty */
docpriv = doc->_private;
assert_true(pcmk_all_flags_set(docpriv->flags, pcmk__xf_dirty));
/* Double check things */
assert_non_null(dtd);
assert_int_equal(dtd->type, XML_DTD_NODE);
/* Check that no private data was created */
priv = dtd->_private;
assert_null(priv);
/* Clean up */
- /* If you call xmlFreeDtd before xmlFreeDoc, you get a segfault */
- xmlFreeDoc(doc);
+ // If you call xmlFreeDtd() before pcmk__xml_free_doc(), you get a segfault
+ pcmk__xml_free_doc(doc);
}
static void
create_cdata_node(void **state) {
xml_doc_private_t *docpriv = NULL;
xml_node_private_t *priv = NULL;
xmlDoc *doc = pcmk__xml_new_doc();
xmlNodePtr node = xmlNewCDataBlock(doc, (pcmkXmlStr) "blahblah", 8);
/* Adding a node to the document marks it as dirty */
docpriv = doc->_private;
assert_true(pcmk_all_flags_set(docpriv->flags, pcmk__xf_dirty));
/* Double check things */
assert_non_null(node);
assert_int_equal(node->type, XML_CDATA_SECTION_NODE);
/* Check that no private data was created */
priv = node->_private;
assert_null(priv);
/* Clean up */
- pcmk__xml_free(node);
- xmlFreeDoc(doc);
+ pcmk__xml_free_doc(doc);
}
// The group setup/teardown functions call pcmk__xml_init()/pcmk__cml_xleanup()
PCMK__UNIT_TEST(pcmk__xml_test_setup_group, pcmk__xml_test_teardown_group,
cmocka_unit_test(buffer_scheme_test),
cmocka_unit_test(create_element_node),
cmocka_unit_test(create_attr_node),
cmocka_unit_test(create_comment_node),
cmocka_unit_test(create_text_node),
cmocka_unit_test(create_dtd_node),
cmocka_unit_test(create_cdata_node));
diff --git a/lib/common/tests/xml/pcmk__xml_new_doc_test.c b/lib/common/tests/xml/pcmk__xml_new_doc_test.c
index dd65647b45..762aec4007 100644
--- a/lib/common/tests/xml/pcmk__xml_new_doc_test.c
+++ b/lib/common/tests/xml/pcmk__xml_new_doc_test.c
@@ -1,39 +1,39 @@
/*
* Copyright 2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
* This source code is licensed under the GNU General Public License version 2
* or later (GPLv2+) WITHOUT ANY WARRANTY.
*/
#include <crm_internal.h>
#include <crm/common/unittest_internal.h>
#include "crmcommon_private.h"
/* This tests new_private_data() indirectly for document nodes. Testing
* free_private_data() would be much less straightforward and is not worth the
* hassle.
*/
static void
create_document_node(void **state) {
xml_doc_private_t *docpriv = NULL;
xmlDoc *doc = pcmk__xml_new_doc();
assert_non_null(doc);
assert_int_equal(doc->type, XML_DOCUMENT_NODE);
docpriv = doc->_private;
assert_non_null(docpriv);
assert_int_equal(docpriv->check, PCMK__XML_DOC_PRIVATE_MAGIC);
assert_true(pcmk_all_flags_set(docpriv->flags,
pcmk__xf_dirty|pcmk__xf_created));
- xmlFreeDoc(doc);
+ pcmk__xml_free_doc(doc);
}
PCMK__UNIT_TEST(pcmk__xml_test_setup_group, pcmk__xml_test_teardown_group,
cmocka_unit_test(create_document_node))
diff --git a/lib/common/xml.c b/lib/common/xml.c
index ff6e319dca..a88d170086 100644
--- a/lib/common/xml.c
+++ b/lib/common/xml.c
@@ -1,2634 +1,2651 @@
/*
* Copyright 2004-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
* This source code is licensed under the GNU Lesser General Public License
* version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
*/
#include <crm_internal.h>
#include <stdarg.h>
#include <stdint.h> // uint32_t
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h> // stat(), S_ISREG, etc.
#include <sys/types.h>
#include <glib.h> // gboolean, GString
#include <libxml/parser.h>
#include <libxml/tree.h>
#include <libxml/xmlstring.h> // xmlGetUTF8Char()
#include <crm/crm.h>
#include <crm/common/xml.h>
#include <crm/common/xml_internal.h> // PCMK__XML_LOG_BASE, etc.
#include "crmcommon_private.h"
//! libxml2 supports only XML version 1.0, at least as of libxml2-2.12.5
#define XML_VERSION ((pcmkXmlStr) "1.0")
/*!
* \internal
* \brief Apply a function to each XML node in a tree (pre-order, depth-first)
*
* \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.
*/
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)
{
if(xml == NULL || xml->doc == NULL || xml->doc->_private == NULL) {
return FALSE;
} else if (!pcmk_is_set(((xml_doc_private_t *)xml->doc->_private)->flags,
pcmk__xf_tracking)) {
return FALSE;
} else if (lazy && !pcmk_is_set(((xml_doc_private_t *)xml->doc->_private)->flags,
pcmk__xf_lazy)) {
return FALSE;
}
return TRUE;
}
static inline void
set_parent_flag(xmlNode *xml, long flag)
{
for(; xml; xml = xml->parent) {
xml_node_private_t *nodepriv = xml->_private;
if (nodepriv == NULL) {
/* During calls to xmlDocCopyNode(), _private will be unset for parent nodes */
} else {
pcmk__set_xml_flags(nodepriv, flag);
}
}
}
void
pcmk__set_xml_doc_flag(xmlNode *xml, enum xml_private_flags flag)
{
if(xml && xml->doc && xml->doc->_private){
/* During calls to xmlDocCopyNode(), xml->doc may be unset */
xml_doc_private_t *docpriv = xml->doc->_private;
pcmk__set_xml_flags(docpriv, flag);
}
}
// Mark document, element, and all element's parents as changed
void
pcmk__mark_xml_node_dirty(xmlNode *xml)
{
pcmk__set_xml_doc_flag(xml, pcmk__xf_dirty);
set_parent_flag(xml, pcmk__xf_dirty);
}
/*!
* \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)
{
xml_node_private_t *nodepriv = xml->_private;
if (nodepriv != NULL) {
nodepriv->flags = pcmk__xf_none;
}
return true;
}
/*!
* \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;
}
/*!
* \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__xml_mark_created(xmlNode *xml)
{
CRM_ASSERT(xml != NULL);
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);
}
// Free an XML object previously marked as deleted
static void
free_deleted_object(void *data)
{
if(data) {
pcmk__deleted_xml_t *deleted_obj = data;
g_free(deleted_obj->path);
free(deleted_obj);
}
}
// Free and NULL user, ACLs, and deleted objects in an XML node's private data
static void
reset_xml_private_data(xml_doc_private_t *docpriv)
{
if (docpriv != NULL) {
CRM_ASSERT(docpriv->check == PCMK__XML_DOC_PRIVATE_MAGIC);
free(docpriv->user);
docpriv->user = NULL;
if (docpriv->acls != NULL) {
pcmk__free_acls(docpriv->acls);
docpriv->acls = NULL;
}
if(docpriv->deleted_objs) {
g_list_free_full(docpriv->deleted_objs, free_deleted_object);
docpriv->deleted_objs = NULL;
}
}
}
// Free all private data associated with an XML node
static void
free_private_data(xmlNode *node)
{
/* Note:
This function frees private data assosciated with an XML node,
unless the function is being called as a result of internal
XSLT cleanup.
That could happen through, for example, the following chain of
function calls:
xsltApplyStylesheetInternal
-> xsltFreeTransformContext
-> xsltFreeRVTs
-> xmlFreeDoc
And in that case, the node would fulfill three conditions:
1. It would be a standalone document (i.e. it wouldn't be
part of a document)
2. It would have a space-prefixed name (for reference, please
see xsltInternals.h: XSLT_MARK_RES_TREE_FRAG)
3. It would carry its own payload in the _private field.
We do not free data in this circumstance to avoid a failed
assertion on the PCMK__XML_*_PRIVATE_MAGIC later.
*/
if (node->name == NULL || node->name[0] != ' ') {
if (node->_private) {
if (node->type == XML_DOCUMENT_NODE) {
reset_xml_private_data(node->_private);
} else {
CRM_ASSERT(((xml_node_private_t *) node->_private)->check
== PCMK__XML_NODE_PRIVATE_MAGIC);
/* nothing dynamically allocated nested */
}
free(node->_private);
node->_private = NULL;
}
}
}
// Allocate and initialize private data for an XML node
static void
new_private_data(xmlNode *node)
{
switch (node->type) {
case XML_DOCUMENT_NODE: {
xml_doc_private_t *docpriv =
pcmk__assert_alloc(1, sizeof(xml_doc_private_t));
docpriv->check = PCMK__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);
node->_private = docpriv;
break;
}
case XML_ELEMENT_NODE:
case XML_ATTRIBUTE_NODE:
case XML_COMMENT_NODE: {
xml_node_private_t *nodepriv =
pcmk__assert_alloc(1, sizeof(xml_node_private_t));
nodepriv->check = PCMK__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);
node->_private = nodepriv;
if (pcmk__tracking_xml_changes(node, FALSE)) {
/* XML_ELEMENT_NODE doesn't get picked up here, node->doc is
* not hooked up at the point we are called
*/
pcmk__mark_xml_node_dirty(node);
}
break;
}
case XML_TEXT_NODE:
case XML_DTD_NODE:
case XML_CDATA_SECTION_NODE:
break;
default:
/* Ignore */
crm_trace("Ignoring %p %d", node, node->type);
CRM_LOG_ASSERT(node->type == XML_ELEMENT_NODE);
break;
}
}
void
xml_track_changes(xmlNode * xml, const char *user, xmlNode *acl_source, bool enforce_acls)
{
xml_accept_changes(xml);
crm_trace("Tracking changes%s to %p", enforce_acls?" with ACLs":"", xml);
pcmk__set_xml_doc_flag(xml, pcmk__xf_tracking);
if(enforce_acls) {
if(acl_source == NULL) {
acl_source = xml;
}
pcmk__set_xml_doc_flag(xml, pcmk__xf_acl_enabled);
pcmk__unpack_acl(acl_source, xml, user);
pcmk__apply_acl(xml);
}
}
bool xml_tracking_changes(xmlNode * xml)
{
return (xml != NULL) && (xml->doc != NULL) && (xml->doc->_private != NULL)
&& pcmk_is_set(((xml_doc_private_t *)(xml->doc->_private))->flags,
pcmk__xf_tracking);
}
bool xml_document_dirty(xmlNode *xml)
{
return (xml != NULL) && (xml->doc != NULL) && (xml->doc->_private != NULL)
&& pcmk_is_set(((xml_doc_private_t *)(xml->doc->_private))->flags,
pcmk__xf_dirty);
}
/*!
* \internal
* \brief Return ordinal position of an XML node among its siblings
*
* \param[in] xml XML node to check
* \param[in] ignore_if_set Don't count siblings with this flag set
*
* \return Ordinal position of \p xml (starting with 0)
*/
int
pcmk__xml_position(const xmlNode *xml, enum xml_private_flags ignore_if_set)
{
int position = 0;
for (const xmlNode *cIter = xml; cIter->prev; cIter = cIter->prev) {
xml_node_private_t *nodepriv = ((xmlNode*)cIter->prev)->_private;
if (!pcmk_is_set(nodepriv->flags, ignore_if_set)) {
position++;
}
}
return position;
}
/*!
* \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)
{
reset_xml_node_flags(xml, NULL);
pcmk__xe_remove_matching_attrs(xml, pcmk__marked_as_deleted, NULL);
return true;
}
/*!
* \internal
* \brief Find first child XML node matching another given XML node
*
* \param[in] haystack XML whose children should be checked
* \param[in] needle XML to match (comment content or element name and ID)
* \param[in] exact If true and needle is a comment, position must match
*/
xmlNode *
pcmk__xml_match(const xmlNode *haystack, const xmlNode *needle, bool exact)
{
CRM_CHECK(needle != NULL, return NULL);
if (needle->type == XML_COMMENT_NODE) {
return pcmk__xc_match(haystack, needle, exact);
} else {
const char *id = pcmk__xe_id(needle);
const char *attr = (id == NULL)? NULL : PCMK_XA_ID;
return pcmk__xe_first_child(haystack, (const char *) needle->name, attr,
id);
}
}
void
xml_accept_changes(xmlNode * xml)
{
xmlNode *top = NULL;
xml_doc_private_t *docpriv = NULL;
if(xml == NULL) {
return;
}
crm_trace("Accepting changes to %p", xml);
docpriv = xml->doc->_private;
top = xmlDocGetRootElement(xml->doc);
reset_xml_private_data(xml->doc->_private);
if (!pcmk_is_set(docpriv->flags, pcmk__xf_dirty)) {
docpriv->flags = pcmk__xf_none;
return;
}
docpriv->flags = pcmk__xf_none;
pcmk__xml_tree_foreach(top, accept_attr_deletions, NULL);
}
/*!
* \internal
* \brief Find first XML child element matching given criteria
*
* \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 \c NULL if none found
*/
xmlNode *
pcmk__xe_first_child(const xmlNode *parent, const char *node_name,
const char *attr_n, const char *attr_v)
{
xmlNode *child = NULL;
const char *parent_name = "<null>";
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;
}
for (; child != NULL; child = pcmk__xe_next(child)) {
const char *value = NULL;
if ((node_name != NULL) && !pcmk__xe_is(child, node_name)) {
// Node name mismatch
continue;
}
if (attr_n == NULL) {
// No attribute match needed
return child;
}
value = crm_element_value(child, attr_n);
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;
}
}
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;
}
/*!
* \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().
*
* 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++".
*
* \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)
*/
int
pcmk__xe_set_score(xmlNode *target, const char *name, const char *value)
{
const char *old_value = NULL;
CRM_CHECK((target != NULL) && (name != NULL), return EINVAL);
if (value == NULL) {
return pcmk_rc_ok;
}
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;
}
/*!
* \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
*/
int
pcmk__xe_copy_attrs(xmlNode *target, const xmlNode *src, uint32_t flags)
{
CRM_CHECK((src != NULL) && (target != NULL), return EINVAL);
for (xmlAttr *attr = pcmk__xe_first_attr(src); attr != NULL;
attr = attr->next) {
const char *name = (const char *) attr->name;
const char *value = pcmk__xml_attr_value(attr);
if (pcmk_is_set(flags, pcmk__xaf_no_overwrite)
&& (crm_element_value(target, name) != NULL)) {
continue;
}
if (pcmk_is_set(flags, pcmk__xaf_score_update)) {
pcmk__xe_set_score(target, name, value);
} else {
crm_xml_add(target, name, value);
}
}
return pcmk_rc_ok;
}
/*!
* \internal
* \brief Move an XML attribute to the end of its element's attribute list
*
* This does not consider ACLs and does not mark the attribute as deleted or
* dirty. Upon return, the attribute still exists and is set to the same value
* as before the call. Only its position in the attribute list may change.
*
* \param[in] data \c pcmk_nvpair_t representing an XML attribute
* \param[in,out] user_data XML element whose attribute to move
*/
static void
move_xml_attr_to_end(gpointer data, gpointer user_data)
{
const pcmk_nvpair_t *pair = data;
xmlNode *xml = user_data;
xmlAttr *attr = xmlHasProp(xml, (pcmkXmlStr) pair->name);
xml_node_private_t *nodepriv = attr->_private;
uint32_t flags = (nodepriv != NULL)? nodepriv->flags : pcmk__xf_none;
xmlRemoveProp(attr);
attr = xmlSetProp(xml, (pcmkXmlStr) pair->name, (pcmkXmlStr) pair->value);
nodepriv = attr->_private;
if (nodepriv != NULL) {
nodepriv->flags = flags;
}
}
/*!
* \internal
* \brief Sort an XML element's attributes by name
*
* This does not consider ACLs and does not mark the attributes as deleted or
* dirty. Upon return, all attributes still exist and are set to the same values
* as before the call. The only thing that may change is the order of the
* attribute list.
*
* \param[in,out] xml XML element whose attributes to sort
*/
void
pcmk__xe_sort_attrs(xmlNode *xml)
{
xmlAttr *attr = pcmk__xe_first_attr(xml);
GSList *nvpairs = NULL;
if ((attr == NULL) || (attr->next == NULL)) {
return;
}
nvpairs = pcmk_xml_attrs2nvpairs(xml);
nvpairs = pcmk_sort_nvpairs(nvpairs);
// Reset attributes in sorted order
g_slist_foreach(nvpairs, move_xml_attr_to_end, xml);
pcmk_free_nvpairs(nvpairs);
}
/*!
* \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 (!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;
}
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);
}
return pcmk_rc_ok;
}
/*!
* \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));
}
}
/*!
* \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;
pcmk__xe_remove_attr(xml, name);
return true;
}
/*!
* \internal
* \brief Remove an XML element's attributes that match some criteria
*
* \param[in,out] element XML element to modify
* \param[in] match If not NULL, only remove attributes for which
* this function returns true
* \param[in,out] user_data Data to pass to \p match
*/
void
pcmk__xe_remove_matching_attrs(xmlNode *element,
bool (*match)(xmlAttrPtr, void *),
void *user_data)
{
xmlAttrPtr next = NULL;
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 (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 *
pcmk__xe_create(xmlNode *parent, const char *name)
{
xmlNode *node = NULL;
CRM_ASSERT(!pcmk__str_empty(name));
if (parent == NULL) {
xmlDoc *doc = pcmk__xml_new_doc();
node = xmlNewDocRawNode(doc, NULL, (pcmkXmlStr) name, NULL);
pcmk__mem_assert(node);
xmlDocSetRootElement(doc, node);
} else {
node = xmlNewChild(parent, NULL, (pcmkXmlStr) name, NULL);
pcmk__mem_assert(node);
}
pcmk__xml_mark_created(node);
return node;
}
/*!
* \internal
* \brief Create a new XML document
*
* \return Newly allocated XML document (guaranteed not to be \c NULL)
*
* \note The caller is responsible for freeing the return value using
- * \c xmlFreeDoc().
+ * \c pcmk__xml_free_doc().
*/
xmlDoc *
pcmk__xml_new_doc(void)
{
/* @TODO Allocate document private data here when we drop
* new_private_data()/free_private_data()
*/
xmlDoc *doc = xmlNewDoc(XML_VERSION);
pcmk__mem_assert(doc);
return doc;
}
+/*!
+ * \internal
+ * \brief Free a new XML document
+ *
+ * \param[in,out] doc XML document to free
+ */
+void
+pcmk__xml_free_doc(xmlDoc *doc)
+{
+ /* @TODO Free document private data here when we drop
+ * new_private_data()/free_private_data()
+ */
+ if (doc != NULL) {
+ xmlFreeDoc(doc);
+ }
+}
+
/*!
* \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, ...)
{
if (node != NULL) {
const char *content = NULL;
char *buf = NULL;
if (strchr(format, '%') == NULL) {
// Nothing to format
content = format;
} else {
va_list ap;
va_start(ap, format);
if (pcmk__str_eq(format, "%s", pcmk__str_none)) {
// No need to make a copy
content = va_arg(ap, const char *);
} else {
CRM_ASSERT(vasprintf(&buf, format, ap) >= 0);
content = buf;
}
va_end(ap);
}
xmlNodeSetContent(node, (pcmkXmlStr) content);
free(buf);
}
}
/*!
* \internal
* \brief Check whether the first character of a string is an XML NameStartChar
*
* See https://www.w3.org/TR/xml/#NT-NameStartChar.
*
* This is almost identical to libxml2's \c xmlIsDocNameStartChar(), but they
* don't expose it as part of the public API.
*
* \param[in] utf8 UTF-8 encoded string
* \param[out] len If not \c NULL, where to store size in bytes of first
* character in \p utf8
*
* \return \c true if \p utf8 begins with a valid XML NameStartChar, or \c false
* otherwise
*/
bool
pcmk__xml_is_name_start_char(const char *utf8, int *len)
{
int c = 0;
int local_len = 0;
if (len == NULL) {
len = &local_len;
}
/* xmlGetUTF8Char() abuses the len argument. At call time, it must be set to
* "the minimum number of bytes present in the sequence... to assure the
* next character is completely contained within the sequence." It's similar
* to the "n" in the strn*() functions. However, this doesn't make any sense
* for null-terminated strings, and there's no value that indicates "keep
* going until '\0'." So we set it to 4, the max number of bytes in a UTF-8
* character.
*
* At return, it's set to the actual number of bytes in the char, or 0 on
* error.
*/
*len = 4;
// Note: xmlGetUTF8Char() assumes a 32-bit int
c = xmlGetUTF8Char((pcmkXmlStr) utf8, len);
if (c < 0) {
GString *buf = g_string_sized_new(32);
for (int i = 0; (i < 4) && (utf8[i] != '\0'); i++) {
g_string_append_printf(buf, " 0x%.2X", utf8[i]);
}
crm_info("Invalid UTF-8 character (bytes:%s)",
(pcmk__str_empty(buf->str)? " <none>" : buf->str));
g_string_free(buf, TRUE);
return false;
}
return (c == '_')
|| (c == ':')
|| ((c >= 'a') && (c <= 'z'))
|| ((c >= 'A') && (c <= 'Z'))
|| ((c >= 0xC0) && (c <= 0xD6))
|| ((c >= 0xD8) && (c <= 0xF6))
|| ((c >= 0xF8) && (c <= 0x2FF))
|| ((c >= 0x370) && (c <= 0x37D))
|| ((c >= 0x37F) && (c <= 0x1FFF))
|| ((c >= 0x200C) && (c <= 0x200D))
|| ((c >= 0x2070) && (c <= 0x218F))
|| ((c >= 0x2C00) && (c <= 0x2FEF))
|| ((c >= 0x3001) && (c <= 0xD7FF))
|| ((c >= 0xF900) && (c <= 0xFDCF))
|| ((c >= 0xFDF0) && (c <= 0xFFFD))
|| ((c >= 0x10000) && (c <= 0xEFFFF));
}
/*!
* \internal
* \brief Check whether the first character of a string is an XML NameChar
*
* See https://www.w3.org/TR/xml/#NT-NameChar.
*
* This is almost identical to libxml2's \c xmlIsDocNameChar(), but they don't
* expose it as part of the public API.
*
* \param[in] utf8 UTF-8 encoded string
* \param[out] len If not \c NULL, where to store size in bytes of first
* character in \p utf8
*
* \return \c true if \p utf8 begins with a valid XML NameChar, or \c false
* otherwise
*/
bool
pcmk__xml_is_name_char(const char *utf8, int *len)
{
int c = 0;
int local_len = 0;
if (len == NULL) {
len = &local_len;
}
// See comment regarding len in pcmk__xml_is_name_start_char()
*len = 4;
// Note: xmlGetUTF8Char() assumes a 32-bit int
c = xmlGetUTF8Char((pcmkXmlStr) utf8, len);
if (c < 0) {
GString *buf = g_string_sized_new(32);
for (int i = 0; (i < 4) && (utf8[i] != '\0'); i++) {
g_string_append_printf(buf, " 0x%.2X", utf8[i]);
}
crm_info("Invalid UTF-8 character (bytes:%s)",
(pcmk__str_empty(buf->str)? " <none>" : buf->str));
g_string_free(buf, TRUE);
return false;
}
return ((c >= 'a') && (c <= 'z'))
|| ((c >= 'A') && (c <= 'Z'))
|| ((c >= '0') && (c <= '9'))
|| (c == '_')
|| (c == ':')
|| (c == '-')
|| (c == '.')
|| (c == 0xB7)
|| ((c >= 0xC0) && (c <= 0xD6))
|| ((c >= 0xD8) && (c <= 0xF6))
|| ((c >= 0xF8) && (c <= 0x2FF))
|| ((c >= 0x300) && (c <= 0x36F))
|| ((c >= 0x370) && (c <= 0x37D))
|| ((c >= 0x37F) && (c <= 0x1FFF))
|| ((c >= 0x200C) && (c <= 0x200D))
|| ((c >= 0x203F) && (c <= 0x2040))
|| ((c >= 0x2070) && (c <= 0x218F))
|| ((c >= 0x2C00) && (c <= 0x2FEF))
|| ((c >= 0x3001) && (c <= 0xD7FF))
|| ((c >= 0xF900) && (c <= 0xFDCF))
|| ((c >= 0xFDF0) && (c <= 0xFFFD))
|| ((c >= 0x10000) && (c <= 0xEFFFF));
}
/*!
* \internal
* \brief Sanitize a string so it is usable as an XML ID
*
* An ID must match the Name production as defined here:
* https://www.w3.org/TR/xml/#NT-Name.
*
* Convert an invalid start character to \c '_'. Convert an invalid character
* after the start character to \c '.'.
*
* \param[in,out] id String to sanitize
*/
void
pcmk__xml_sanitize_id(char *id)
{
bool valid = true;
int len = 0;
// If id is empty or NULL, there's no way to make it a valid XML ID
CRM_ASSERT(!pcmk__str_empty(id));
/* @TODO Suppose there are two strings and each has an invalid ID character
* in the same position. The strings are otherwise identical. Both strings
* will be sanitized to the same valid ID, which is incorrect.
*
* The caller is responsible for ensuring the sanitized ID does not already
* exist in a given XML document before using it, if uniqueness is desired.
*/
valid = pcmk__xml_is_name_start_char(id, &len);
CRM_CHECK(len > 0, return); // UTF-8 encoding error
if (!valid) {
*id = '_';
for (int i = 1; i < len; i++) {
id[i] = '.';
}
}
for (id += len; *id != '\0'; id += len) {
valid = pcmk__xml_is_name_char(id, &len);
CRM_CHECK(len > 0, return); // UTF-8 encoding error
if (!valid) {
for (int i = 0; i < len; i++) {
id[i] = '.';
}
}
}
}
/*!
* \internal
* \brief Set a formatted string as an XML element's ID
*
* If the formatted string would not be a valid ID, it's first sanitized by
* \c pcmk__xml_sanitize_id().
*
* \param[in,out] node Node whose ID to set
* \param[in] format <tt>printf(3)</tt>-style format string
* \param[in] ... Arguments for \p format
*/
G_GNUC_PRINTF(2, 3)
void
pcmk__xe_set_id(xmlNode *node, const char *format, ...)
{
char *id = NULL;
va_list ap;
CRM_ASSERT(!pcmk__str_empty(format));
if (node == NULL) {
return;
}
va_start(ap, format);
CRM_ASSERT(vasprintf(&id, format, ap) >= 0);
va_end(ap);
if (!xmlValidateNameValue((pcmkXmlStr) id)) {
pcmk__xml_sanitize_id(id);
}
crm_xml_add(node, PCMK_XA_ID, id);
free(id);
}
/*!
* \internal
* \brief Free an XML tree without ACL checks or change tracking
*
* \param[in,out] xml XML node to free
*/
static void
free_xml_node(xmlNode *xml)
{
/* @TODO Free tree private data here when we drop
* new_private_data()/free_private_data()
*/
xmlUnlinkNode(xml);
xmlFreeNode(xml);
}
/*!
* \internal
* \brief Free an XML tree if ACLs allow; track deletion if tracking is enabled
*
* If \p node is the root of its document, free the entire document.
*
* \param[in,out] node XML node to free
* \param[in] position Position of \p node among its siblings for change
* tracking (negative to calculate automatically if
* needed)
*/
static void
free_xml_with_position(xmlNode *node, int position)
{
xmlDoc *doc = NULL;
xml_node_private_t *nodepriv = NULL;
if (node == NULL) {
return;
}
doc = node->doc;
nodepriv = node->_private;
if ((doc != NULL) && (xmlDocGetRootElement(doc) == node)) {
/* @TODO Should we check ACLs first? Otherwise it seems like we could
* free the root element without write permission.
*/
- xmlFreeDoc(doc);
+ pcmk__xml_free_doc(doc);
return;
}
if (!pcmk__check_acl(node, NULL, pcmk__xf_acl_write)) {
GString *xpath = NULL;
pcmk__if_tracing({}, return);
xpath = pcmk__element_xpath(node);
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 ((doc != NULL) && pcmk__tracking_xml_changes(node, false)
&& !pcmk_is_set(nodepriv->flags, pcmk__xf_created)) {
xml_doc_private_t *docpriv = doc->_private;
GString *xpath = pcmk__element_xpath(node);
if (xpath != NULL) {
pcmk__deleted_xml_t *deleted_obj = NULL;
crm_trace("Deleting %s %p from %p", xpath->str, node, doc);
deleted_obj = pcmk__assert_alloc(1, sizeof(pcmk__deleted_xml_t));
deleted_obj->path = g_string_free(xpath, FALSE);
deleted_obj->position = -1;
// Record the position only for XML comments for now
if (node->type == XML_COMMENT_NODE) {
if (position >= 0) {
deleted_obj->position = position;
} else {
deleted_obj->position = pcmk__xml_position(node,
pcmk__xf_skip);
}
}
docpriv->deleted_objs = g_list_append(docpriv->deleted_objs,
deleted_obj);
pcmk__set_xml_doc_flag(node, pcmk__xf_dirty);
}
}
free_xml_node(node);
}
/*!
* \internal
* \brief Free an XML tree if ACLs allow; track deletion if tracking is enabled
*
* If \p xml is the root of its document, free the entire document.
*
* \param[in,out] xml XML node to free
*/
void
pcmk__xml_free(xmlNode *xml)
{
free_xml_with_position(xml, -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 *
pcmk__xml_copy(xmlNode *parent, xmlNode *src)
{
xmlNode *copy = NULL;
if (src == NULL) {
return NULL;
}
if (parent == NULL) {
xmlDoc *doc = NULL;
// The copy will be the root element of a new document
CRM_ASSERT(src->type == XML_ELEMENT_NODE);
doc = pcmk__xml_new_doc();
copy = xmlDocCopyNode(src, doc, 1);
pcmk__mem_assert(copy);
xmlDocSetRootElement(doc, copy);
} else {
copy = xmlDocCopyNode(src, parent->doc, 1);
pcmk__mem_assert(copy);
xmlAddChild(parent, copy);
}
pcmk__xml_mark_created(copy);
return copy;
}
/*!
* \internal
* \brief Remove XML text nodes from specified XML and all its children
*
* \param[in,out] xml XML to strip text from
*/
void
pcmk__strip_xml_text(xmlNode *xml)
{
xmlNode *iter = xml->children;
while (iter) {
xmlNode *next = iter->next;
switch (iter->type) {
case XML_TEXT_NODE:
free_xml_node(iter);
break;
case XML_ELEMENT_NODE:
/* Search it */
pcmk__strip_xml_text(iter);
break;
default:
/* Leave it */
break;
}
iter = next;
}
}
/*!
* \internal
* \brief Add a "last written" attribute to an XML element, set to current time
*
* \param[in,out] xe XML element to add attribute to
*
* \return Value that was set, or NULL on error
*/
const char *
pcmk__xe_add_last_written(xmlNode *xe)
{
char *now_s = pcmk__epoch2str(NULL, 0);
const char *result = NULL;
result = crm_xml_add(xe, PCMK_XA_CIB_LAST_WRITTEN,
pcmk__s(now_s, "Could not determine current time"));
free(now_s);
return result;
}
/*!
* \internal
* \brief Check whether a string has XML special characters that must be escaped
*
* See \c pcmk__xml_escape() and \c pcmk__xml_escape_type for more details.
*
* \param[in] text String to check
* \param[in] type Type of escaping
*
* \return \c true if \p text has special characters that need to be escaped, or
* \c false otherwise
*/
bool
pcmk__xml_needs_escape(const char *text, enum pcmk__xml_escape_type type)
{
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;
case pcmk__xml_escape_attr:
switch (*text) {
case '<':
case '>':
case '&':
case '"':
return true;
default:
if (g_ascii_iscntrl(*text)) {
return true;
}
break;
}
break;
case pcmk__xml_escape_attr_pretty:
switch (*text) {
case '\n':
case '\r':
case '\t':
case '"':
return true;
default:
break;
}
break;
default: // Invalid enum value
CRM_ASSERT(false);
break;
}
text = g_utf8_next_char(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 \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().
*/
gchar *
pcmk__xml_escape(const char *text, enum pcmk__xml_escape_type type)
{
GString *copy = NULL;
if (text == NULL) {
return NULL;
}
copy = g_string_sized_new(strlen(text));
while (*text != '\0') {
// Don't escape any non-ASCII characters
if ((*text & 0x80) != 0) {
size_t bytes = g_utf8_next_char(text) - text;
g_string_append_len(copy, text, bytes);
text += bytes;
continue;
}
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;
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;
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;
default: // Invalid enum value
CRM_ASSERT(false);
break;
}
text = g_utf8_next_char(text);
}
return g_string_free(copy, FALSE);
}
/*!
* \internal
* \brief Set a flag on all attributes of an XML element
*
* \param[in,out] xml XML node to set flags on
* \param[in] flag XML private flag to set
*/
static void
set_attrs_flag(xmlNode *xml, enum xml_private_flags flag)
{
for (xmlAttr *attr = pcmk__xe_first_attr(xml); attr; attr = attr->next) {
pcmk__set_xml_flags((xml_node_private_t *) (attr->_private), flag);
}
}
/*!
* \internal
* \brief Add an XML attribute to a node, marked as deleted
*
* When calculating XML changes, we need to know when an attribute has been
* deleted. Add the attribute back to the new XML, so that we can check the
* removal against ACLs, and mark it as deleted for later removal after
* differences have been calculated.
*
* \param[in,out] new_xml XML to modify
* \param[in] element Name of XML element that changed (for logging)
* \param[in] attr_name Name of attribute that was deleted
* \param[in] old_value Value of attribute that was deleted
*/
static void
mark_attr_deleted(xmlNode *new_xml, const char *element, const char *attr_name,
const char *old_value)
{
xml_doc_private_t *docpriv = new_xml->doc->_private;
xmlAttr *attr = NULL;
xml_node_private_t *nodepriv;
// Prevent the dirty flag being set recursively upwards
pcmk__clear_xml_flags(docpriv, pcmk__xf_tracking);
// Restore the old value (and the tracking flag)
attr = xmlSetProp(new_xml, (pcmkXmlStr) attr_name, (pcmkXmlStr) old_value);
pcmk__set_xml_flags(docpriv, pcmk__xf_tracking);
// Reset flags (so the attribute doesn't appear as newly created)
nodepriv = attr->_private;
nodepriv->flags = 0;
// Check ACLs and mark restored value for later removal
remove_xe_attr(new_xml, attr);
crm_trace("XML attribute %s=%s was removed from %s",
attr_name, old_value, element);
}
/*
* \internal
* \brief Check ACLs for a changed XML attribute
*/
static void
mark_attr_changed(xmlNode *new_xml, const char *element, const char *attr_name,
const char *old_value)
{
char *vcopy = crm_element_value_copy(new_xml, attr_name);
crm_trace("XML attribute %s was changed from '%s' to '%s' in %s",
attr_name, old_value, vcopy, element);
// Restore the original value
xmlSetProp(new_xml, (pcmkXmlStr) attr_name, (pcmkXmlStr) old_value);
// Change it back to the new value, to check ACLs
crm_xml_add(new_xml, attr_name, vcopy);
free(vcopy);
}
/*!
* \internal
* \brief Mark an XML attribute as having changed position
*
* \param[in,out] new_xml XML to modify
* \param[in] element Name of XML element that changed (for logging)
* \param[in,out] old_attr Attribute that moved, in original XML
* \param[in,out] new_attr Attribute that moved, in \p new_xml
* \param[in] p_old Ordinal position of \p old_attr in original XML
* \param[in] p_new Ordinal position of \p new_attr in \p new_xml
*/
static void
mark_attr_moved(xmlNode *new_xml, const char *element, xmlAttr *old_attr,
xmlAttr *new_attr, int p_old, int p_new)
{
xml_node_private_t *nodepriv = new_attr->_private;
crm_trace("XML attribute %s moved from position %d to %d in %s",
old_attr->name, p_old, p_new, element);
// Mark document, element, and all element's parents as changed
pcmk__mark_xml_node_dirty(new_xml);
// Mark attribute as changed
pcmk__set_xml_flags(nodepriv, pcmk__xf_dirty|pcmk__xf_moved);
nodepriv = (p_old > p_new)? old_attr->_private : new_attr->_private;
pcmk__set_xml_flags(nodepriv, pcmk__xf_skip);
}
/*!
* \internal
* \brief Calculate differences in all previously existing XML attributes
*
* \param[in,out] old_xml Original XML to compare
* \param[in,out] new_xml New XML to compare
*/
static void
xml_diff_old_attrs(xmlNode *old_xml, xmlNode *new_xml)
{
xmlAttr *attr_iter = pcmk__xe_first_attr(old_xml);
while (attr_iter != NULL) {
const char *name = (const char *) attr_iter->name;
xmlAttr *old_attr = attr_iter;
xmlAttr *new_attr = xmlHasProp(new_xml, attr_iter->name);
const char *old_value = pcmk__xml_attr_value(attr_iter);
attr_iter = attr_iter->next;
if (new_attr == NULL) {
mark_attr_deleted(new_xml, (const char *) old_xml->name, name,
old_value);
} else {
xml_node_private_t *nodepriv = new_attr->_private;
int new_pos = pcmk__xml_position((xmlNode*) new_attr,
pcmk__xf_skip);
int old_pos = pcmk__xml_position((xmlNode*) old_attr,
pcmk__xf_skip);
const char *new_value = crm_element_value(new_xml, name);
// This attribute isn't new
pcmk__clear_xml_flags(nodepriv, pcmk__xf_created);
if (strcmp(new_value, old_value) != 0) {
mark_attr_changed(new_xml, (const char *) old_xml->name, name,
old_value);
} else if ((old_pos != new_pos)
&& !pcmk__tracking_xml_changes(new_xml, TRUE)) {
mark_attr_moved(new_xml, (const char *) old_xml->name,
old_attr, new_attr, old_pos, new_pos);
}
}
}
}
/*!
* \internal
* \brief Check all attributes in new XML for creation
*
* For each of a given XML element's attributes marked as newly created, accept
* (and mark as dirty) or reject the creation according to ACLs.
*
* \param[in,out] new_xml XML to check
*/
static void
mark_created_attrs(xmlNode *new_xml)
{
xmlAttr *attr_iter = pcmk__xe_first_attr(new_xml);
while (attr_iter != NULL) {
xmlAttr *new_attr = attr_iter;
xml_node_private_t *nodepriv = attr_iter->_private;
attr_iter = attr_iter->next;
if (pcmk_is_set(nodepriv->flags, pcmk__xf_created)) {
const char *attr_name = (const char *) new_attr->name;
crm_trace("Created new attribute %s=%s in %s",
attr_name, pcmk__xml_attr_value(new_attr),
new_xml->name);
/* Check ACLs (we can't use the remove-then-create trick because it
* would modify the attribute position).
*/
if (pcmk__check_acl(new_xml, attr_name, pcmk__xf_acl_write)) {
pcmk__mark_xml_attr_dirty(new_attr);
} else {
// Creation was not allowed, so remove the attribute
xmlUnsetProp(new_xml, new_attr->name);
}
}
}
}
/*!
* \internal
* \brief Calculate differences in attributes between two XML nodes
*
* \param[in,out] old_xml Original XML to compare
* \param[in,out] new_xml New XML to compare
*/
static void
xml_diff_attrs(xmlNode *old_xml, xmlNode *new_xml)
{
set_attrs_flag(new_xml, pcmk__xf_created); // cleared later if not really new
xml_diff_old_attrs(old_xml, new_xml);
mark_created_attrs(new_xml);
}
/*!
* \internal
* \brief Add an XML child element to a node, marked as deleted
*
* When calculating XML changes, we need to know when a child element has been
* deleted. Add the child back to the new XML, so that we can check the removal
* against ACLs, and mark it as deleted for later removal after differences have
* been calculated.
*
* \param[in,out] old_child Child element from original XML
* \param[in,out] new_parent New XML to add marked copy to
*/
static void
mark_child_deleted(xmlNode *old_child, xmlNode *new_parent)
{
// Re-create the child element so we can check ACLs
xmlNode *candidate = pcmk__xml_copy(new_parent, old_child);
// Clear flags on new child and its children
pcmk__xml_tree_foreach(candidate, reset_xml_node_flags, NULL);
// Check whether ACLs allow the deletion
pcmk__apply_acl(xmlDocGetRootElement(candidate->doc));
// Remove the child again (which will track it in document's deleted_objs)
free_xml_with_position(candidate,
pcmk__xml_position(old_child, pcmk__xf_skip));
if (pcmk__xml_match(new_parent, old_child, true) == NULL) {
pcmk__set_xml_flags((xml_node_private_t *) (old_child->_private),
pcmk__xf_skip);
}
}
static void
mark_child_moved(xmlNode *old_child, xmlNode *new_parent, xmlNode *new_child,
int p_old, int p_new)
{
xml_node_private_t *nodepriv = new_child->_private;
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);
if (p_old > p_new) {
nodepriv = old_child->_private;
} else {
nodepriv = new_child->_private;
}
pcmk__set_xml_flags(nodepriv, pcmk__xf_skip);
}
// Given original and new XML, mark new XML portions that have changed
static void
mark_xml_changes(xmlNode *old_xml, xmlNode *new_xml, bool check_top)
{
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__xml_mark_created(new_xml);
pcmk__apply_creation_acl(new_xml, check_top);
return;
}
nodepriv = new_xml->_private;
CRM_CHECK(nodepriv != NULL, return);
if(nodepriv->flags & pcmk__xf_processed) {
/* Avoid re-comparing nodes */
return;
}
pcmk__set_xml_flags(nodepriv, pcmk__xf_processed);
xml_diff_attrs(old_xml, new_xml);
// Check for differences in the original children
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);
if (new_child != NULL) {
mark_xml_changes(old_child, new_child, true);
} else {
mark_child_deleted(old_child, new_xml);
}
}
// Check for moved or created children
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);
if (old_child == NULL) {
// This is a newly created child
nodepriv = new_child->_private;
pcmk__set_xml_flags(nodepriv, pcmk__xf_skip);
// May free new_child
mark_xml_changes(old_child, new_child, true);
} else {
/* Check for movement, we already checked for differences */
int p_new = pcmk__xml_position(new_child, pcmk__xf_skip);
int p_old = pcmk__xml_position(old_child, pcmk__xf_skip);
if(p_old != p_new) {
mark_child_moved(old_child, new_xml, new_child, p_old, p_new);
}
}
new_child = next;
}
}
void
xml_calculate_significant_changes(xmlNode *old_xml, xmlNode *new_xml)
{
pcmk__set_xml_doc_flag(new_xml, pcmk__xf_lazy);
xml_calculate_changes(old_xml, new_xml);
}
// Called functions may set the \p pcmk__xf_skip flag on parts of \p old_xml
void
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(pcmk__xe_id(old_xml), pcmk__xe_id(new_xml),
pcmk__str_none),
return);
if(xml_tracking_changes(new_xml) == FALSE) {
xml_track_changes(new_xml, NULL, NULL, FALSE);
}
mark_xml_changes(old_xml, new_xml, FALSE);
}
/*!
* \internal
* \brief Find a comment with matching content in specified XML
*
* \param[in] root XML to search
* \param[in] search_comment Comment whose content should be searched for
* \param[in] exact If true, comment must also be at same position
*/
xmlNode *
pcmk__xc_match(const xmlNode *root, const xmlNode *search_comment, bool exact)
{
xmlNode *a_child = NULL;
int search_offset = pcmk__xml_position(search_comment, pcmk__xf_skip);
CRM_CHECK(search_comment->type == XML_COMMENT_NODE, return NULL);
for (a_child = pcmk__xml_first_child(root); a_child != NULL;
a_child = pcmk__xml_next(a_child)) {
if (exact) {
int offset = pcmk__xml_position(a_child, pcmk__xf_skip);
xml_node_private_t *nodepriv = a_child->_private;
if (offset < search_offset) {
continue;
} else if (offset > search_offset) {
return NULL;
}
if (pcmk_is_set(nodepriv->flags, pcmk__xf_skip)) {
continue;
}
}
if (a_child->type == XML_COMMENT_NODE
&& pcmk__str_eq((const char *)a_child->content, (const char *)search_comment->content, pcmk__str_casei)) {
return a_child;
} else if (exact) {
return NULL;
}
}
return NULL;
}
/*!
* \internal
* \brief Make one XML comment match another (in content)
*
* \param[in,out] parent If \p target is NULL and this is not, add or update
* comment child of this XML node that matches \p update
* \param[in,out] target If not NULL, update this XML comment node
* \param[in] update Make comment content match this (must not be NULL)
*
* \note At least one of \parent and \target must be non-NULL
*/
void
pcmk__xc_update(xmlNode *parent, xmlNode *target, xmlNode *update)
{
CRM_CHECK(update != NULL, return);
CRM_CHECK(update->type == XML_COMMENT_NODE, return);
if (target == NULL) {
target = pcmk__xc_match(parent, update, false);
}
if (target == NULL) {
pcmk__xml_copy(parent, update);
} else if (!pcmk__str_eq((const char *)target->content, (const char *)update->content, pcmk__str_casei)) {
xmlFree(target->content);
target->content = xmlStrdup(update->content);
}
}
/*!
* \internal
* \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 \c NULL)
* \param[in] flags Group of <tt>enum pcmk__xa_flags</tt>
*
* \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.
*/
static void
update_xe(xmlNode *parent, xmlNode *target, xmlNode *update, uint32_t flags)
{
// @TODO Try to refactor further, possibly using pcmk__xml_tree_foreach()
const char *update_name = NULL;
const char *update_id_attr = NULL;
const char *update_id_val = NULL;
char *trace_s = NULL;
crm_log_xml_trace(update, "update");
crm_log_xml_trace(target, "target");
CRM_CHECK(update != NULL, goto done);
if (update->type == XML_COMMENT_NODE) {
pcmk__xc_update(parent, target, update);
goto done;
}
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 {
update_id_val = crm_element_value(update, PCMK_XA_ID_REF);
if (update_id_val != NULL) {
update_id_attr = PCMK_XA_ID_REF;
}
}
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) {
// Recursive call
target = pcmk__xe_first_child(parent, update_name, update_id_attr,
update_id_val);
}
if (target == NULL) {
// 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 {
// 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);
pcmk__xe_copy_attrs(target, update, flags);
for (xmlNode *child = pcmk__xml_first_child(update); child != NULL;
child = pcmk__xml_next(child)) {
crm_trace("Updating child of %s", pcmk__s(trace_s, update_name));
update_xe(target, NULL, child, flags);
}
crm_trace("Finished with %s", pcmk__s(trace_s, update_name));
done:
free(trace_s);
}
/*!
* \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;
if (!pcmk__xe_is(search, (const char *) xml->name)) {
// No match: either not both elements, or different element types
return true;
}
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;
}
}
crm_log_xml_trace(xml, "delete-match");
crm_log_xml_trace(search, "delete-search");
pcmk__xml_free(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
pcmk__xe_delete_match(xmlNode *xml, xmlNode *search)
{
// See @COMPAT comment in pcmk__xe_replace_match()
CRM_CHECK((xml != NULL) && (search != 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, delete_xe_if_matching, search)) {
// Found and deleted an element
return pcmk_rc_ok;
}
}
// No match found in this subtree
return ENXIO;
}
/*!
* \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);
// 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);
free_xml_node(old);
}
/*!
* \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)
{
xmlNode *replace = user_data;
const char *xml_id = NULL;
const char *replace_id = NULL;
xml_id = pcmk__xe_id(xml);
replace_id = pcmk__xe_id(replace);
if (!pcmk__xe_is(replace, (const char *) xml->name)) {
// No match: either not both elements, or different element types
return true;
}
if ((replace_id != NULL)
&& !pcmk__str_eq(replace_id, xml_id, pcmk__str_none)) {
// No match: ID was provided in replace and doesn't match xml's ID
return true;
}
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;
}
}
// No match found in this subtree
return ENXIO;
}
//! 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>
};
/*!
* \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 update_xe()).
*
* 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;
if (!pcmk__xe_is(update, (const char *) xml->name)) {
// No match: either not both elements, or different element types
return true;
}
if (!pcmk__str_eq(pcmk__xe_id(xml), pcmk__xe_id(update), pcmk__str_none)) {
// No match: ID mismatch
return true;
}
crm_log_xml_trace(xml, "update-match");
crm_log_xml_trace(update, "update-with");
update_xe(NULL, xml, update, data->flags);
// Found a match and replaced it; stop traversing tree
return false;
}
/*!
* \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 update_xe()).
*
* 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,
};
CRM_CHECK((xml != NULL) && (update != NULL), return EINVAL);
if (!pcmk__xml_tree_foreach(xml, update_xe_if_matching, &data)) {
// Found and updated an element
return pcmk_rc_ok;
}
// No match found in this subtree
return ENXIO;
}
/*!
* \internal
* \brief Get next sibling XML element with the same name as a given element
*
* \param[in] node XML element to start from
*
* \return Next sibling XML element with same name
*/
xmlNode *
pcmk__xe_next_same(const xmlNode *node)
{
for (xmlNode *match = pcmk__xe_next(node); match != NULL;
match = pcmk__xe_next(match)) {
if (pcmk__xe_is(match, (const char *) node->name)) {
return match;
}
}
return NULL;
}
/*!
* \internal
* \brief Initialize the Pacemaker XML environment
*
* Set an XML buffer allocation scheme, set XML node create and destroy
* callbacks, and load schemas into the cache.
*/
void
pcmk__xml_init(void)
{
// @TODO Try to find a better caller than crm_log_preinit()
static bool initialized = false;
if (!initialized) {
initialized = true;
/* Double the buffer size when the buffer needs to grow. The default
* allocator XML_BUFFER_ALLOC_EXACT was found to cause poor performance
* due to the number of reallocs.
*/
xmlSetBufferAllocationScheme(XML_BUFFER_ALLOC_DOUBLEIT);
// Initialize private data at node creation
xmlRegisterNodeDefault(new_private_data);
// Free private data at node destruction
xmlDeregisterNodeDefault(free_private_data);
// Load schemas into the cache
pcmk__schema_init();
}
}
/*!
* \internal
* \brief Tear down the Pacemaker XML environment
*
* Destroy schema cache and clean up memory allocated by libxml2.
*/
void
pcmk__xml_cleanup(void)
{
pcmk__schema_cleanup();
xmlCleanupParser();
}
char *
pcmk__xml_artefact_root(enum pcmk__xml_artefact_ns ns)
{
static const char *base = NULL;
char *ret = NULL;
if (base == NULL) {
base = pcmk__env_option(PCMK__ENV_SCHEMA_DIRECTORY);
}
if (pcmk__str_empty(base)) {
base = PCMK_SCHEMA_DIR;
}
switch (ns) {
case pcmk__xml_artefact_ns_legacy_rng:
case pcmk__xml_artefact_ns_legacy_xslt:
ret = strdup(base);
break;
case pcmk__xml_artefact_ns_base_rng:
case pcmk__xml_artefact_ns_base_xslt:
ret = crm_strdup_printf("%s/base", base);
break;
default:
crm_err("XML artefact family specified as %u not recognized", ns);
}
return ret;
}
static char *
find_artefact(enum pcmk__xml_artefact_ns ns, const char *path, const char *filespec)
{
char *ret = NULL;
switch (ns) {
case pcmk__xml_artefact_ns_legacy_rng:
case pcmk__xml_artefact_ns_base_rng:
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:
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;
}
void
pcmk__xe_set_propv(xmlNodePtr node, va_list pairs)
{
while (true) {
const char *name, *value;
name = va_arg(pairs, const char *);
if (name == NULL) {
return;
}
value = va_arg(pairs, const char *);
if (value != NULL) {
crm_xml_add(node, name, value);
}
}
}
void
pcmk__xe_set_props(xmlNodePtr node, ...)
{
va_list pairs;
va_start(pairs, node);
pcmk__xe_set_propv(node, pairs);
va_end(pairs);
}
int
pcmk__xe_foreach_child(xmlNode *xml, const char *child_element_name,
int (*handler)(xmlNode *xml, void *userdata),
void *userdata)
{
xmlNode *children = (xml? xml->children : NULL);
CRM_ASSERT(handler != NULL);
for (xmlNode *node = children; node != NULL; node = node->next) {
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) {
return rc;
}
}
}
return pcmk_rc_ok;
}
// Deprecated functions kept only for backward API compatibility
// LCOV_EXCL_START
#include <crm/common/xml_compat.h>
xmlNode *
copy_xml(xmlNode *src)
{
xmlDoc *doc = pcmk__xml_new_doc();
xmlNode *copy = NULL;
copy = xmlDocCopyNode(src, doc, 1);
pcmk__mem_assert(copy);
xmlDocSetRootElement(doc, copy);
return copy;
}
void
crm_xml_init(void)
{
pcmk__xml_init();
}
void
crm_xml_cleanup(void)
{
pcmk__xml_cleanup();
}
void
pcmk_free_xml_subtree(xmlNode *xml)
{
free_xml_node(xml);
}
void
free_xml(xmlNode *child)
{
pcmk__xml_free(child);
}
xmlNode *
expand_idref(xmlNode *input, xmlNode *top)
{
return pcmk__xe_resolve_idref(input, top);
}
void
crm_xml_sanitize_id(char *id)
{
char *c;
for (c = id; *c; ++c) {
switch (*c) {
case ':':
case '#':
*c = '.';
}
}
}
void
crm_xml_set_id(xmlNode *xml, const char *format, ...)
{
va_list ap;
int len = 0;
char *id = NULL;
/* equivalent to crm_strdup_printf() */
va_start(ap, format);
len = vasprintf(&id, format, ap);
va_end(ap);
CRM_ASSERT(len > 0);
crm_xml_sanitize_id(id);
crm_xml_add(xml, PCMK_XA_ID, id);
free(id);
}
xmlNode *
sorted_xml(xmlNode *input, xmlNode *parent, gboolean recursive)
{
xmlNode *child = NULL;
GSList *nvpairs = NULL;
xmlNode *result = NULL;
CRM_CHECK(input != NULL, return NULL);
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__xe_first_child(input, NULL, NULL, NULL); child != NULL;
child = pcmk__xe_next(child)) {
if (recursive) {
sorted_xml(child, result, recursive);
} else {
pcmk__xml_copy(result, child);
}
}
return result;
}
// LCOV_EXCL_STOP
// End deprecated API
diff --git a/lib/pacemaker/pcmk_acl.c b/lib/pacemaker/pcmk_acl.c
index 801262e579..5b8cd1797d 100644
--- a/lib/pacemaker/pcmk_acl.c
+++ b/lib/pacemaker/pcmk_acl.c
@@ -1,397 +1,398 @@
/*
* Copyright 2004-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
* This source code is licensed under the GNU Lesser General Public License
* version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
*/
#include <crm_internal.h>
#include <stdio.h>
#include <sys/types.h>
#include <pwd.h>
#include <string.h>
#include <stdlib.h>
#include <stdarg.h>
#include <libxml/parser.h>
#include <libxml/tree.h>
#include <libxml/xpath.h>
#include <libxslt/transform.h>
#include <libxslt/variables.h>
#include <libxslt/xsltutils.h>
#include <crm/crm.h>
#include <crm/common/xml.h>
#include <crm/common/xml_internal.h>
#include <crm/common/internal.h>
#include <pacemaker-internal.h>
#define ACL_NS_PREFIX "http://clusterlabs.org/ns/pacemaker/access/"
#define ACL_NS_Q_PREFIX "pcmk-access-"
#define ACL_NS_Q_WRITABLE (const xmlChar *) ACL_NS_Q_PREFIX "writable"
#define ACL_NS_Q_READABLE (const xmlChar *) ACL_NS_Q_PREFIX "readable"
#define ACL_NS_Q_DENIED (const xmlChar *) ACL_NS_Q_PREFIX "denied"
static const xmlChar *NS_WRITABLE = (const xmlChar *) ACL_NS_PREFIX "writable";
static const xmlChar *NS_READABLE = (const xmlChar *) ACL_NS_PREFIX "readable";
static const xmlChar *NS_DENIED = (const xmlChar *) ACL_NS_PREFIX "denied";
/*!
* \brief This function takes a node and marks it with the namespace
* given in the ns parameter.
*
* \param[in,out] i_node
* \param[in] ns
* \param[in,out] ret
* \param[in,out] ns_recycle_writable
* \param[in,out] ns_recycle_readable
* \param[in,out] ns_recycle_denied
*/
static void
pcmk__acl_mark_node_with_namespace(xmlNode *i_node, const xmlChar *ns, int *ret,
xmlNs **ns_recycle_writable,
xmlNs **ns_recycle_readable,
xmlNs **ns_recycle_denied)
{
if (ns == NS_WRITABLE)
{
if (*ns_recycle_writable == NULL)
{
*ns_recycle_writable = xmlNewNs(xmlDocGetRootElement(i_node->doc),
NS_WRITABLE, ACL_NS_Q_WRITABLE);
}
xmlSetNs(i_node, *ns_recycle_writable);
*ret = pcmk_rc_ok;
}
else if (ns == NS_READABLE)
{
if (*ns_recycle_readable == NULL)
{
*ns_recycle_readable = xmlNewNs(xmlDocGetRootElement(i_node->doc),
NS_READABLE, ACL_NS_Q_READABLE);
}
xmlSetNs(i_node, *ns_recycle_readable);
*ret = pcmk_rc_ok;
}
else if (ns == NS_DENIED)
{
if (*ns_recycle_denied == NULL)
{
*ns_recycle_denied = xmlNewNs(xmlDocGetRootElement(i_node->doc),
NS_DENIED, ACL_NS_Q_DENIED);
};
xmlSetNs(i_node, *ns_recycle_denied);
*ret = pcmk_rc_ok;
}
}
/*!
* \brief Annotate a given XML element or property and its siblings with
* XML namespaces to indicate ACL permissions
*
* \param[in,out] xml_modify XML to annotate
*
* \return A standard Pacemaker return code
* Namely:
* - pcmk_rc_ok upon success,
* - pcmk_rc_already if ACLs were not applicable,
* - pcmk_rc_schema_validation if the validation schema version
* is unsupported (see note), or
* - EINVAL or ENOMEM as appropriate;
*
* \note This function is recursive
*/
static int
annotate_with_siblings(xmlNode *xml_modify)
{
static xmlNs *ns_recycle_writable = NULL,
*ns_recycle_readable = NULL,
*ns_recycle_denied = NULL;
static const xmlDoc *prev_doc = NULL;
xmlNode *i_node = NULL;
const xmlChar *ns;
int ret = EINVAL; // nodes have not been processed yet
if (prev_doc == NULL || prev_doc != xml_modify->doc) {
prev_doc = xml_modify->doc;
ns_recycle_writable = ns_recycle_readable = ns_recycle_denied = NULL;
}
for (i_node = xml_modify; i_node != NULL; i_node = i_node->next) {
switch (i_node->type) {
case XML_ELEMENT_NODE:
pcmk__set_xml_doc_flag(i_node, pcmk__xf_tracking);
if (!pcmk__check_acl(i_node, NULL, pcmk__xf_acl_read)) {
ns = NS_DENIED;
} else if (!pcmk__check_acl(i_node, NULL, pcmk__xf_acl_write)) {
ns = NS_READABLE;
} else {
ns = NS_WRITABLE;
}
pcmk__acl_mark_node_with_namespace(i_node, ns, &ret,
&ns_recycle_writable,
&ns_recycle_readable,
&ns_recycle_denied);
// @TODO Could replace recursion with iteration to save stack
if (i_node->properties != NULL) {
/* This is not entirely clear, but relies on the very same
* class-hierarchy emulation that libxml2 has firmly baked
* in its API/ABI
*/
ret |= annotate_with_siblings((xmlNodePtr)
i_node->properties);
}
if (i_node->children != NULL) {
ret |= annotate_with_siblings(i_node->children);
}
break;
case XML_ATTRIBUTE_NODE:
// We can utilize that parent has already been assigned the ns
if (!pcmk__check_acl(i_node->parent,
(const char *) i_node->name,
pcmk__xf_acl_read)) {
ns = NS_DENIED;
} else if (!pcmk__check_acl(i_node,
(const char *) i_node->name,
pcmk__xf_acl_write)) {
ns = NS_READABLE;
} else {
ns = NS_WRITABLE;
}
pcmk__acl_mark_node_with_namespace(i_node, ns, &ret,
&ns_recycle_writable,
&ns_recycle_readable,
&ns_recycle_denied);
break;
case XML_COMMENT_NODE:
// We can utilize that parent has already been assigned the ns
if (!pcmk__check_acl(i_node->parent,
(const char *) i_node->name,
pcmk__xf_acl_read)) {
ns = NS_DENIED;
} else if (!pcmk__check_acl(i_node->parent,
(const char *) i_node->name,
pcmk__xf_acl_write)) {
ns = NS_READABLE;
} else {
ns = NS_WRITABLE;
}
pcmk__acl_mark_node_with_namespace(i_node, ns, &ret,
&ns_recycle_writable,
&ns_recycle_readable,
&ns_recycle_denied);
break;
default:
break;
}
}
return ret;
}
int
pcmk__acl_annotate_permissions(const char *cred, const xmlDoc *cib_doc,
xmlDoc **acl_evaled_doc)
{
int ret;
xmlNode *target, *comment;
const char *validation;
CRM_CHECK(cred != NULL, return EINVAL);
CRM_CHECK(cib_doc != NULL, return EINVAL);
CRM_CHECK(acl_evaled_doc != NULL, return EINVAL);
/* avoid trivial accidental XML injection */
if (strpbrk(cred, "<>&") != NULL) {
return EINVAL;
}
if (!pcmk_acl_required(cred)) {
/* nothing to evaluate */
return pcmk_rc_already;
}
validation = crm_element_value(xmlDocGetRootElement(cib_doc),
PCMK_XA_VALIDATE_WITH);
if (pcmk__cmp_schemas_by_name(PCMK__COMPAT_ACL_2_MIN_INCL,
validation) > 0) {
return pcmk_rc_schema_validation;
}
target = pcmk__xml_copy(NULL, xmlDocGetRootElement((xmlDoc *) cib_doc));
if (target == NULL) {
return EINVAL;
}
pcmk__enable_acl(target, target, cred);
ret = annotate_with_siblings(target);
if (ret == pcmk_rc_ok) {
char *credentials = crm_strdup_printf("ACLs as evaluated for user %s",
cred);
comment = xmlNewDocComment(target->doc, (pcmkXmlStr) credentials);
free(credentials);
if (comment == NULL) {
pcmk__xml_free(target);
return EINVAL;
}
xmlAddPrevSibling(xmlDocGetRootElement(target->doc), comment);
*acl_evaled_doc = target->doc;
return pcmk_rc_ok;
} else {
pcmk__xml_free(target);
return ret; //for now, it should be some kind of error
}
}
int
pcmk__acl_evaled_render(xmlDoc *annotated_doc, enum pcmk__acl_render_how how,
xmlChar **doc_txt_ptr)
{
xmlDoc *xslt_doc;
xsltStylesheet *xslt;
xsltTransformContext *xslt_ctxt;
xmlDoc *res;
char *sfile;
static const char *params_namespace[] = {
"accessrendercfg:c-writable", ACL_NS_Q_PREFIX "writable:",
"accessrendercfg:c-readable", ACL_NS_Q_PREFIX "readable:",
"accessrendercfg:c-denied", ACL_NS_Q_PREFIX "denied:",
"accessrendercfg:c-reset", "",
"accessrender:extra-spacing", "no",
"accessrender:self-reproducing-prefix", ACL_NS_Q_PREFIX,
NULL
}, *params_useansi[] = {
/* start with hard-coded defaults, then adapt per the template ones */
"accessrendercfg:c-writable", "\x1b[32m",
"accessrendercfg:c-readable", "\x1b[34m",
"accessrendercfg:c-denied", "\x1b[31m",
"accessrendercfg:c-reset", "\x1b[0m",
"accessrender:extra-spacing", "no",
"accessrender:self-reproducing-prefix", ACL_NS_Q_PREFIX,
NULL
}, *params_noansi[] = {
"accessrendercfg:c-writable", "vvv---[ WRITABLE ]---vvv",
"accessrendercfg:c-readable", "vvv---[ READABLE ]---vvv",
"accessrendercfg:c-denied", "vvv---[ ~DENIED~ ]---vvv",
"accessrendercfg:c-reset", "",
"accessrender:extra-spacing", "yes",
"accessrender:self-reproducing-prefix", "",
NULL
};
const char **params;
int rc = pcmk_rc_ok;
xmlParserCtxtPtr parser_ctxt;
/* unfortunately, the input (coming from CIB originally) was parsed with
blanks ignored, and since the output is a conversion of XML to text
format (we would be covered otherwise thanks to implicit
pretty-printing), we need to dump the tree to string output first,
only to subsequently reparse it -- this time with blanks honoured */
xmlChar *annotated_dump;
int dump_size;
CRM_ASSERT(how != pcmk__acl_render_none);
// Color is the default render mode for terminals; text is default otherwise
if (how == pcmk__acl_render_default) {
if (isatty(STDOUT_FILENO)) {
how = pcmk__acl_render_color;
} else {
how = pcmk__acl_render_text;
}
}
xmlDocDumpFormatMemory(annotated_doc, &annotated_dump, &dump_size, 1);
res = xmlReadDoc(annotated_dump, "on-the-fly-access-render", NULL,
XML_PARSE_NONET);
CRM_ASSERT(res != NULL);
xmlFree(annotated_dump);
- xmlFreeDoc(annotated_doc);
+ pcmk__xml_free_doc(annotated_doc);
annotated_doc = res;
sfile = pcmk__xml_artefact_path(pcmk__xml_artefact_ns_base_xslt,
"access-render-2");
parser_ctxt = xmlNewParserCtxt();
CRM_ASSERT(sfile != NULL);
pcmk__mem_assert(parser_ctxt);
xslt_doc = xmlCtxtReadFile(parser_ctxt, sfile, NULL, XML_PARSE_NONET);
xslt = xsltParseStylesheetDoc(xslt_doc); /* acquires xslt_doc! */
if (xslt == NULL) {
crm_crit("Problem in parsing %s", sfile);
rc = EINVAL;
goto done;
}
xmlFreeParserCtxt(parser_ctxt);
xslt_ctxt = xsltNewTransformContext(xslt, annotated_doc);
pcmk__mem_assert(xslt_ctxt);
switch (how) {
case pcmk__acl_render_namespace:
params = params_namespace;
break;
case pcmk__acl_render_text:
params = params_noansi;
break;
default:
/* pcmk__acl_render_color is the only remaining option.
* The compiler complains about params possibly uninitialized if we
* don't use default here.
*/
params = params_useansi;
break;
}
xsltQuoteUserParams(xslt_ctxt, params);
res = xsltApplyStylesheetUser(xslt, annotated_doc, NULL,
NULL, NULL, xslt_ctxt);
- xmlFreeDoc(annotated_doc);
+ pcmk__xml_free_doc(annotated_doc);
annotated_doc = NULL;
xsltFreeTransformContext(xslt_ctxt);
xslt_ctxt = NULL;
if (how == pcmk__acl_render_color && params != params_useansi) {
char **param_i = (char **) params;
do {
free(*param_i);
} while (*param_i++ != NULL);
free(params);
}
if (res == NULL) {
rc = EINVAL;
} else {
int doc_txt_len;
int temp = xsltSaveResultToString(doc_txt_ptr, &doc_txt_len, res, xslt);
- xmlFreeDoc(res);
+
+ pcmk__xml_free_doc(res);
if (temp != 0) {
rc = EINVAL;
}
}
done:
if (xslt != NULL) {
xsltFreeStylesheet(xslt);
}
free(sfile);
return rc;
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Sat, Nov 23, 4:16 PM (21 h, 14 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1018867
Default Alt Text
(126 KB)
Attached To
Mode
rP Pacemaker
Attached
Detach File
Event Timeline
Log In to Comment