/src/pacemaker/lib/common/xml_display.c
Line | Count | Source |
1 | | /* |
2 | | * Copyright 2004-2025 the Pacemaker project contributors |
3 | | * |
4 | | * The version control history for this file may have further details. |
5 | | * |
6 | | * This source code is licensed under the GNU Lesser General Public License |
7 | | * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. |
8 | | */ |
9 | | |
10 | | #include <crm_internal.h> |
11 | | |
12 | | #include <libxml/tree.h> |
13 | | |
14 | | #include <crm/crm.h> |
15 | | #include <crm/common/xml.h> |
16 | | #include <crm/common/xml_internal.h> // PCMK__XML_LOG_BASE, etc. |
17 | | #include "crmcommon_private.h" |
18 | | |
19 | | static int show_xml_node(pcmk__output_t *out, GString *buffer, |
20 | | const char *prefix, const xmlNode *data, int depth, |
21 | | uint32_t options); |
22 | | |
23 | | // Log an XML library error |
24 | | void |
25 | | pcmk__log_xmllib_err(void *ctx, const char *fmt, ...) |
26 | 0 | { |
27 | 0 | va_list ap; |
28 | |
|
29 | 0 | va_start(ap, fmt); |
30 | 0 | pcmk__if_tracing( |
31 | 0 | { |
32 | 0 | PCMK__XML_LOG_BASE(LOG_ERR, TRUE, |
33 | 0 | crm_abort(__FILE__, __PRETTY_FUNCTION__, |
34 | 0 | __LINE__, "xml library error", TRUE, |
35 | 0 | TRUE), |
36 | 0 | "XML Error: ", fmt, ap); |
37 | 0 | }, |
38 | 0 | { |
39 | 0 | PCMK__XML_LOG_BASE(LOG_ERR, TRUE, 0, "XML Error: ", fmt, ap); |
40 | 0 | } |
41 | 0 | ); |
42 | 0 | va_end(ap); |
43 | 0 | } |
44 | | |
45 | | /*! |
46 | | * \internal |
47 | | * \brief Output an XML comment with depth-based indentation |
48 | | * |
49 | | * \param[in,out] out Output object |
50 | | * \param[in] data XML node to output |
51 | | * \param[in] depth Current indentation level |
52 | | * \param[in] options Group of \p pcmk__xml_fmt_options flags |
53 | | * |
54 | | * \return Standard Pacemaker return code |
55 | | * |
56 | | * \note This currently produces output only for text-like output objects. |
57 | | */ |
58 | | static int |
59 | | show_xml_comment(pcmk__output_t *out, const xmlNode *data, int depth, |
60 | | uint32_t options) |
61 | 0 | { |
62 | 0 | if (pcmk__is_set(options, pcmk__xml_fmt_open)) { |
63 | 0 | int width = |
64 | 0 | pcmk__is_set(options, pcmk__xml_fmt_pretty)? (2 * depth) : 0; |
65 | |
|
66 | 0 | return out->info(out, "%*s<!--%s-->", |
67 | 0 | width, "", (const char *) data->content); |
68 | 0 | } |
69 | 0 | return pcmk_rc_no_output; |
70 | 0 | } |
71 | | |
72 | | /*! |
73 | | * \internal |
74 | | * \brief Output an XML element in a formatted way |
75 | | * |
76 | | * \param[in,out] out Output object |
77 | | * \param[in,out] buffer Where to build output strings |
78 | | * \param[in] prefix String to prepend to every line of output |
79 | | * \param[in] data XML node to output |
80 | | * \param[in] depth Current indentation level |
81 | | * \param[in] options Group of \p pcmk__xml_fmt_options flags |
82 | | * |
83 | | * \return Standard Pacemaker return code |
84 | | * |
85 | | * \note This is a recursive helper function for \p show_xml_node(). |
86 | | * \note This currently produces output only for text-like output objects. |
87 | | * \note \p buffer may be overwritten many times. The caller is responsible for |
88 | | * freeing it using \p g_string_free() but should not rely on its |
89 | | * contents. |
90 | | */ |
91 | | static int |
92 | | show_xml_element(pcmk__output_t *out, GString *buffer, const char *prefix, |
93 | | const xmlNode *data, int depth, uint32_t options) |
94 | 0 | { |
95 | 0 | int spaces = pcmk__is_set(options, pcmk__xml_fmt_pretty)? (2 * depth) : 0; |
96 | 0 | int rc = pcmk_rc_no_output; |
97 | |
|
98 | 0 | if (pcmk__is_set(options, pcmk__xml_fmt_open)) { |
99 | 0 | const char *hidden = pcmk__xe_get(data, PCMK__XA_HIDDEN); |
100 | |
|
101 | 0 | g_string_truncate(buffer, 0); |
102 | |
|
103 | 0 | for (int lpc = 0; lpc < spaces; lpc++) { |
104 | 0 | g_string_append_c(buffer, ' '); |
105 | 0 | } |
106 | 0 | pcmk__g_strcat(buffer, "<", data->name, NULL); |
107 | |
|
108 | 0 | for (const xmlAttr *attr = pcmk__xe_first_attr(data); attr != NULL; |
109 | 0 | attr = attr->next) { |
110 | 0 | xml_node_private_t *nodepriv = attr->_private; |
111 | 0 | const char *p_name = (const char *) attr->name; |
112 | 0 | const char *p_value = pcmk__xml_attr_value(attr); |
113 | 0 | gchar *p_copy = NULL; |
114 | |
|
115 | 0 | if ((nodepriv == NULL) |
116 | 0 | || pcmk__is_set(nodepriv->flags, pcmk__xf_deleted)) { |
117 | 0 | continue; |
118 | 0 | } |
119 | | |
120 | 0 | if ((hidden != NULL) && (p_name[0] != '\0') |
121 | 0 | && (strstr(hidden, p_name) != NULL)) { |
122 | |
|
123 | 0 | p_value = "*****"; |
124 | |
|
125 | 0 | } else { |
126 | 0 | p_copy = pcmk__xml_escape(p_value, true); |
127 | 0 | p_value = p_copy; |
128 | 0 | } |
129 | |
|
130 | 0 | pcmk__g_strcat(buffer, " ", p_name, "=\"", |
131 | 0 | pcmk__s(p_value, "<null>"), "\"", NULL); |
132 | 0 | g_free(p_copy); |
133 | 0 | } |
134 | |
|
135 | 0 | if ((data->children != NULL) |
136 | 0 | && pcmk__is_set(options, pcmk__xml_fmt_children)) { |
137 | 0 | g_string_append_c(buffer, '>'); |
138 | |
|
139 | 0 | } else { |
140 | 0 | g_string_append(buffer, "/>"); |
141 | 0 | } |
142 | |
|
143 | 0 | rc = out->info(out, "%s%s%s", |
144 | 0 | pcmk__s(prefix, ""), pcmk__str_empty(prefix)? "" : " ", |
145 | 0 | buffer->str); |
146 | 0 | } |
147 | |
|
148 | 0 | if (data->children == NULL) { |
149 | 0 | return rc; |
150 | 0 | } |
151 | | |
152 | 0 | if (pcmk__is_set(options, pcmk__xml_fmt_children)) { |
153 | 0 | for (const xmlNode *child = pcmk__xml_first_child(data); child != NULL; |
154 | 0 | child = pcmk__xml_next(child)) { |
155 | |
|
156 | 0 | int temp_rc = show_xml_node(out, buffer, prefix, child, depth + 1, |
157 | 0 | options |
158 | 0 | |pcmk__xml_fmt_open |
159 | 0 | |pcmk__xml_fmt_close); |
160 | 0 | rc = pcmk__output_select_rc(rc, temp_rc); |
161 | 0 | } |
162 | 0 | } |
163 | |
|
164 | 0 | if (pcmk__is_set(options, pcmk__xml_fmt_close)) { |
165 | 0 | int temp_rc = out->info(out, "%s%s%*s</%s>", |
166 | 0 | pcmk__s(prefix, ""), |
167 | 0 | pcmk__str_empty(prefix)? "" : " ", |
168 | 0 | spaces, "", data->name); |
169 | 0 | rc = pcmk__output_select_rc(rc, temp_rc); |
170 | 0 | } |
171 | |
|
172 | 0 | return rc; |
173 | 0 | } |
174 | | |
175 | | /*! |
176 | | * \internal |
177 | | * \brief Output an XML element or comment in a formatted way |
178 | | * |
179 | | * \param[in,out] out Output object |
180 | | * \param[in,out] buffer Where to build output strings |
181 | | * \param[in] prefix String to prepend to every line of output |
182 | | * \param[in] data XML node to log |
183 | | * \param[in] depth Current indentation level |
184 | | * \param[in] options Group of \p pcmk__xml_fmt_options flags |
185 | | * |
186 | | * \return Standard Pacemaker return code |
187 | | * |
188 | | * \note This is a recursive helper function for \p pcmk__xml_show(). |
189 | | * \note This currently produces output only for text-like output objects. |
190 | | * \note \p buffer may be overwritten many times. The caller is responsible for |
191 | | * freeing it using \p g_string_free() but should not rely on its |
192 | | * contents. |
193 | | */ |
194 | | static int |
195 | | show_xml_node(pcmk__output_t *out, GString *buffer, const char *prefix, |
196 | | const xmlNode *data, int depth, uint32_t options) |
197 | 0 | { |
198 | 0 | switch (data->type) { |
199 | 0 | case XML_COMMENT_NODE: |
200 | 0 | return show_xml_comment(out, data, depth, options); |
201 | 0 | case XML_ELEMENT_NODE: |
202 | 0 | return show_xml_element(out, buffer, prefix, data, depth, options); |
203 | 0 | default: |
204 | 0 | return pcmk_rc_no_output; |
205 | 0 | } |
206 | 0 | } |
207 | | |
208 | | /*! |
209 | | * \internal |
210 | | * \brief Output an XML element or comment in a formatted way |
211 | | * |
212 | | * \param[in,out] out Output object |
213 | | * \param[in] prefix String to prepend to every line of output |
214 | | * \param[in] data XML node to output |
215 | | * \param[in] depth Current nesting level |
216 | | * \param[in] options Group of \p pcmk__xml_fmt_options flags |
217 | | * |
218 | | * \return Standard Pacemaker return code |
219 | | * |
220 | | * \note This currently produces output only for text-like output objects. |
221 | | */ |
222 | | int |
223 | | pcmk__xml_show(pcmk__output_t *out, const char *prefix, const xmlNode *data, |
224 | | int depth, uint32_t options) |
225 | 0 | { |
226 | 0 | int rc = pcmk_rc_no_output; |
227 | 0 | GString *buffer = NULL; |
228 | |
|
229 | 0 | pcmk__assert(out != NULL); |
230 | 0 | CRM_CHECK(depth >= 0, depth = 0); |
231 | |
|
232 | 0 | if (data == NULL) { |
233 | 0 | return rc; |
234 | 0 | } |
235 | | |
236 | | /* Allocate a buffer once, for show_xml_node() to truncate and reuse in |
237 | | * recursive calls |
238 | | */ |
239 | 0 | buffer = g_string_sized_new(1024); |
240 | 0 | rc = show_xml_node(out, buffer, prefix, data, depth, options); |
241 | 0 | g_string_free(buffer, TRUE); |
242 | |
|
243 | 0 | return rc; |
244 | 0 | } |
245 | | |
246 | | /*! |
247 | | * \internal |
248 | | * \brief Output XML portions that have been marked as changed |
249 | | * |
250 | | * \param[in,out] out Output object |
251 | | * \param[in] data XML node to output |
252 | | * \param[in] depth Current indentation level |
253 | | * \param[in] options Group of \p pcmk__xml_fmt_options flags |
254 | | * |
255 | | * \note This is a recursive helper for \p pcmk__xml_show_changes(), showing |
256 | | * changes to \p data and its children. |
257 | | * \note This currently produces output only for text-like output objects. |
258 | | */ |
259 | | static int |
260 | | show_xml_changes_recursive(pcmk__output_t *out, const xmlNode *data, int depth, |
261 | | uint32_t options) |
262 | 0 | { |
263 | | /* @COMPAT: When log_data_element() is removed, we can remove the options |
264 | | * argument here and instead hard-code pcmk__xml_log_pretty. |
265 | | */ |
266 | 0 | xml_node_private_t *nodepriv = (xml_node_private_t *) data->_private; |
267 | 0 | int rc = pcmk_rc_no_output; |
268 | 0 | int temp_rc = pcmk_rc_no_output; |
269 | |
|
270 | 0 | if (nodepriv == NULL) { |
271 | 0 | return pcmk_rc_no_output; |
272 | 0 | } |
273 | | |
274 | 0 | if (pcmk__all_flags_set(nodepriv->flags, pcmk__xf_dirty|pcmk__xf_created)) { |
275 | | // Newly created |
276 | 0 | return pcmk__xml_show(out, PCMK__XML_PREFIX_CREATED, data, depth, |
277 | 0 | options |
278 | 0 | |pcmk__xml_fmt_open |
279 | 0 | |pcmk__xml_fmt_children |
280 | 0 | |pcmk__xml_fmt_close); |
281 | 0 | } |
282 | | |
283 | 0 | if (pcmk__is_set(nodepriv->flags, pcmk__xf_dirty)) { |
284 | | // Modified or moved |
285 | 0 | bool pretty = pcmk__is_set(options, pcmk__xml_fmt_pretty); |
286 | 0 | int spaces = pretty? (2 * depth) : 0; |
287 | 0 | const char *prefix = PCMK__XML_PREFIX_MODIFIED; |
288 | |
|
289 | 0 | if (pcmk__is_set(nodepriv->flags, pcmk__xf_moved)) { |
290 | 0 | prefix = PCMK__XML_PREFIX_MOVED; |
291 | 0 | } |
292 | | |
293 | | // Log opening tag |
294 | 0 | rc = pcmk__xml_show(out, prefix, data, depth, |
295 | 0 | options|pcmk__xml_fmt_open); |
296 | | |
297 | | // Log changes to attributes |
298 | 0 | for (const xmlAttr *attr = pcmk__xe_first_attr(data); attr != NULL; |
299 | 0 | attr = attr->next) { |
300 | 0 | const char *name = (const char *) attr->name; |
301 | |
|
302 | 0 | nodepriv = attr->_private; |
303 | |
|
304 | 0 | if (pcmk__is_set(nodepriv->flags, pcmk__xf_deleted)) { |
305 | 0 | const char *value = pcmk__xml_attr_value(attr); |
306 | |
|
307 | 0 | temp_rc = out->info(out, "%s %*s @%s=%s", |
308 | 0 | PCMK__XML_PREFIX_DELETED, spaces, "", name, |
309 | 0 | value); |
310 | |
|
311 | 0 | } else if (pcmk__is_set(nodepriv->flags, pcmk__xf_dirty)) { |
312 | 0 | const char *value = pcmk__xml_attr_value(attr); |
313 | |
|
314 | 0 | if (pcmk__is_set(nodepriv->flags, pcmk__xf_created)) { |
315 | 0 | prefix = PCMK__XML_PREFIX_CREATED; |
316 | |
|
317 | 0 | } else if (pcmk__is_set(nodepriv->flags, pcmk__xf_modified)) { |
318 | 0 | prefix = PCMK__XML_PREFIX_MODIFIED; |
319 | |
|
320 | 0 | } else if (pcmk__is_set(nodepriv->flags, pcmk__xf_moved)) { |
321 | 0 | prefix = PCMK__XML_PREFIX_MOVED; |
322 | |
|
323 | 0 | } else { |
324 | 0 | prefix = PCMK__XML_PREFIX_MODIFIED; |
325 | 0 | } |
326 | |
|
327 | 0 | temp_rc = out->info(out, "%s %*s @%s=%s", |
328 | 0 | prefix, spaces, "", name, value); |
329 | 0 | } |
330 | 0 | rc = pcmk__output_select_rc(rc, temp_rc); |
331 | 0 | } |
332 | | |
333 | | // Log changes to children |
334 | 0 | for (const xmlNode *child = pcmk__xml_first_child(data); child != NULL; |
335 | 0 | child = pcmk__xml_next(child)) { |
336 | 0 | temp_rc = show_xml_changes_recursive(out, child, depth + 1, |
337 | 0 | options); |
338 | 0 | rc = pcmk__output_select_rc(rc, temp_rc); |
339 | 0 | } |
340 | | |
341 | | // Log closing tag |
342 | 0 | temp_rc = pcmk__xml_show(out, PCMK__XML_PREFIX_MODIFIED, data, depth, |
343 | 0 | options|pcmk__xml_fmt_close); |
344 | 0 | return pcmk__output_select_rc(rc, temp_rc); |
345 | 0 | } |
346 | | |
347 | | // This node hasn't changed, but check its children |
348 | 0 | for (const xmlNode *child = pcmk__xml_first_child(data); child != NULL; |
349 | 0 | child = pcmk__xml_next(child)) { |
350 | 0 | temp_rc = show_xml_changes_recursive(out, child, depth + 1, options); |
351 | 0 | rc = pcmk__output_select_rc(rc, temp_rc); |
352 | 0 | } |
353 | 0 | return rc; |
354 | 0 | } |
355 | | |
356 | | /*! |
357 | | * \internal |
358 | | * \brief Output changes to an XML node and any children |
359 | | * |
360 | | * \param[in,out] out Output object |
361 | | * \param[in] xml XML node to output |
362 | | * |
363 | | * \return Standard Pacemaker return code |
364 | | * |
365 | | * \note This currently produces output only for text-like output objects. |
366 | | */ |
367 | | int |
368 | | pcmk__xml_show_changes(pcmk__output_t *out, const xmlNode *xml) |
369 | 0 | { |
370 | 0 | xml_doc_private_t *docpriv = NULL; |
371 | 0 | int rc = pcmk_rc_no_output; |
372 | 0 | int temp_rc = pcmk_rc_no_output; |
373 | |
|
374 | 0 | pcmk__assert((out != NULL) && (xml != NULL) && (xml->doc != NULL)); |
375 | |
|
376 | 0 | docpriv = xml->doc->_private; |
377 | 0 | if (!pcmk__is_set(docpriv->flags, pcmk__xf_dirty)) { |
378 | 0 | return rc; |
379 | 0 | } |
380 | | |
381 | 0 | for (const GList *iter = docpriv->deleted_objs; iter != NULL; |
382 | 0 | iter = iter->next) { |
383 | 0 | const pcmk__deleted_xml_t *deleted_obj = iter->data; |
384 | |
|
385 | 0 | if (deleted_obj->position >= 0) { |
386 | 0 | temp_rc = out->info(out, PCMK__XML_PREFIX_DELETED " %s (%d)", |
387 | 0 | deleted_obj->path, deleted_obj->position); |
388 | 0 | } else { |
389 | 0 | temp_rc = out->info(out, PCMK__XML_PREFIX_DELETED " %s", |
390 | 0 | deleted_obj->path); |
391 | 0 | } |
392 | 0 | rc = pcmk__output_select_rc(rc, temp_rc); |
393 | 0 | } |
394 | |
|
395 | 0 | temp_rc = show_xml_changes_recursive(out, xml, 0, pcmk__xml_fmt_pretty); |
396 | 0 | return pcmk__output_select_rc(rc, temp_rc); |
397 | 0 | } |