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