1 /*
2 * Copyright 2019-2026 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 <ctype.h>
13 #include <stdarg.h>
14 #include <stdbool.h>
15 #include <stdlib.h>
16 #include <stdio.h>
17
18 #include <libxml/HTMLtree.h>
19 #include <libxml/tree.h> // xmlNode
20 #include <libxml/xmlstring.h> // xmlChar
21
22 #include <crm/common/xml.h>
23
24 static const char *stylesheet_default =
25 "." PCMK__VALUE_BOLD " { font-weight: bold }\n"
26
27 "." PCMK_VALUE_ONLINE " { color: green }\n"
28 "." PCMK_VALUE_OFFLINE " { color: red }\n"
29 "." PCMK__VALUE_MAINT " { color: blue }\n"
30 "." PCMK_VALUE_STANDBY " { color: blue }\n"
31 "." PCMK__VALUE_HEALTH_RED " { color: red }\n"
32 "." PCMK__VALUE_HEALTH_YELLOW " { color: GoldenRod }\n"
33
34 "." PCMK__VALUE_RSC_FAILED " { color: red }\n"
35 "." PCMK__VALUE_RSC_FAILURE_IGNORED " { color: DarkGreen }\n"
36 "." PCMK__VALUE_RSC_MANAGED " { color: blue }\n"
37 "." PCMK__VALUE_RSC_MULTIPLE " { color: orange }\n"
38 "." PCMK__VALUE_RSC_OK " { color: green }\n"
39
40 "." PCMK__VALUE_WARNING " { color: red; font-weight: bold }";
41
42 /* @TODO stylesheet_link, title, and extra_headers should be set
43 * per-output-object and should be freed before exit
44 */
45 static gboolean cgi_output = FALSE;
46 static gchar *stylesheet_link = NULL;
47 static gchar *title = NULL;
48 static GSList *extra_headers = NULL;
49
50 GOptionEntry pcmk__html_output_entries[] = {
51 { "html-cgi", 0, 0, G_OPTION_ARG_NONE, &cgi_output,
52 "Add CGI headers (requires --output-as=html)",
53 NULL },
54
55 { "html-stylesheet", 0, 0, G_OPTION_ARG_STRING, &stylesheet_link,
56 "Link to an external stylesheet (requires --output-as=html)",
57 "URI" },
58
59 { "html-title", 0, 0, G_OPTION_ARG_STRING, &title,
60 "Specify a page title (requires --output-as=html)",
61 "TITLE" },
62
63 { NULL }
64 };
65
66 /* The first several elements of this struct must be the same as the first
67 * several elements of private_data_s in lib/common/output_xml.c. This
68 * struct gets passed to a bunch of the pcmk__output_xml_* functions which
69 * assume an XML private_data_s. Keeping them laid out the same means this
70 * still works.
71 */
72 typedef struct {
73 /* Begin members that must match the XML version */
74 xmlNode *root;
75 GQueue *parent_q;
76 GSList *errors;
77 /* End members that must match the XML version */
78 } private_data_t;
79
80 static void
81 html_free_priv(pcmk__output_t *out) {
82 private_data_t *priv = NULL;
83
|
(1) Event path: |
Condition "out == NULL", taking false branch. |
|
(2) Event path: |
Condition "out->priv == NULL", taking false branch. |
84 if (out == NULL || out->priv == NULL) {
85 return;
86 }
87
88 priv = out->priv;
89
90 pcmk__xml_free(priv->root);
91 /* The elements of parent_q are xmlNodes that are a part of the
92 * priv->root document, so the above line already frees them. Don't
93 * call g_queue_free_full here.
94 */
95 g_queue_free(priv->parent_q);
96 g_slist_free_full(priv->errors, free);
|
CID (unavailable; MK=59a79fa30534e6d844fa41c1ba2b3155) (#1 of 1): Inconsistent C union access (INCONSISTENT_UNION_ACCESS): |
|
(3) Event assign_union_field: |
The union field "in" of "_pp" is written. |
|
(4) Event inconsistent_union_field_access: |
In "_pp.out", the union field used: "out" is inconsistent with the field most recently stored: "in". |
97 g_clear_pointer(&out->priv, free);
98 }
99
100 static bool
101 html_init(pcmk__output_t *out) {
102 private_data_t *priv = NULL;
103
104 pcmk__assert(out != NULL);
105
106 /* If html_init was previously called on this output struct, just return. */
107 if (out->priv != NULL) {
108 return true;
109 } else {
110 out->priv = calloc(1, sizeof(private_data_t));
111 if (out->priv == NULL) {
112 return false;
113 }
114
115 priv = out->priv;
116 }
117
118 priv->parent_q = g_queue_new();
119
120 priv->root = pcmk__xe_create(NULL, "html");
121 xmlCreateIntSubset(priv->root->doc, (const xmlChar *) "html", NULL, NULL);
122
123 pcmk__xe_set(priv->root, PCMK_XA_LANG, PCMK__VALUE_EN);
124 g_queue_push_tail(priv->parent_q, priv->root);
125 priv->errors = NULL;
126
127 pcmk__output_xml_create_parent(out, "body", NULL);
128
129 return true;
130 }
131
132 static void
133 add_error_node(gpointer data, gpointer user_data) {
134 char *str = (char *) data;
135 pcmk__output_t *out = (pcmk__output_t *) user_data;
136 out->list_item(out, NULL, "%s", str);
137 }
138
139 static void
140 html_finish(pcmk__output_t *out, crm_exit_t exit_status, bool print, void **copy_dest) {
141 private_data_t *priv = NULL;
142 htmlNodePtr head_node = NULL;
143 htmlNodePtr charset_node = NULL;
144 xmlNode *child_node = NULL;
145
146 pcmk__assert(out != NULL);
147
148 priv = out->priv;
149
150 /* If root is NULL, html_init failed and we are being called from pcmk__output_free
151 * in the pcmk__output_new path.
152 */
153 if (priv == NULL || priv->root == NULL) {
154 return;
155 }
156
157 if (cgi_output && print) {
158 fprintf(out->dest, "Content-Type: text/html\n\n");
159 }
160
161 /* Add the head node last - it's not needed earlier because it doesn't contain
162 * anything else that the user could add, and we want it done last to pick up
163 * any options that may have been given.
164 */
165 head_node = pcmk__xe_create(priv->root, "head");
166 xmlAddPrevSibling(priv->root->children, head_node);
167
168 if (title != NULL ) {
169 child_node = pcmk__xe_create(head_node, "title");
170 pcmk__xe_set_content(child_node, "%s", title);
171 } else if (out->request != NULL) {
172 child_node = pcmk__xe_create(head_node, "title");
173 pcmk__xe_set_content(child_node, "%s", out->request);
174 }
175
176 charset_node = pcmk__xe_create(head_node, PCMK__XE_META);
177 pcmk__xe_set(charset_node, "charset", "utf-8");
178
179 /* Add any extra header nodes the caller might have created. */
180 for (GSList *iter = extra_headers; iter != NULL; iter = iter->next) {
181 pcmk__xml_copy(head_node, (xmlNode *) iter->data);
182 }
183
184 /* Stylesheets are included two different ways. The first is via a built-in
185 * default (see the stylesheet_default const above). The second is via the
186 * html-stylesheet option, and this should obviously be a link to a
187 * stylesheet. The second can override the first. At least one should be
188 * given.
189 */
190 child_node = pcmk__xe_create(head_node, "style");
191 pcmk__xe_set_content(child_node, "%s", stylesheet_default);
192
193 if (stylesheet_link != NULL) {
194 htmlNodePtr link_node = pcmk__xe_create(head_node, "link");
195 pcmk__xe_set_props(link_node, "rel", "stylesheet",
196 "href", stylesheet_link,
197 NULL);
198 }
199
200 if (g_slist_length(priv->errors) > 0) {
201 out->begin_list(out, "Errors", NULL, NULL);
202 g_slist_foreach(priv->errors, add_error_node, (gpointer) out);
203 out->end_list(out);
204 }
205
206 if (print) {
207 htmlDocDump(out->dest, priv->root->doc);
208 }
209
210 if (copy_dest != NULL) {
211 *copy_dest = pcmk__xml_copy(NULL, priv->root);
212 }
213
214 g_slist_free_full(extra_headers, (GDestroyNotify) pcmk__xml_free);
215 extra_headers = NULL;
216 }
217
218 static void
219 html_reset(pcmk__output_t *out) {
220 pcmk__assert(out != NULL);
221
222 out->dest = freopen(NULL, "w", out->dest);
223 pcmk__assert(out->dest != NULL);
224
225 html_free_priv(out);
226 html_init(out);
227 }
228
229 static void
230 html_subprocess_output(pcmk__output_t *out, int exit_status,
231 const char *proc_stdout, const char *proc_stderr) {
232 char *rc_buf = NULL;
233
234 pcmk__assert(out != NULL);
235
236 rc_buf = pcmk__assert_asprintf("Return code: %d", exit_status);
237
238 pcmk__output_create_xml_text_node(out, "h2", "Command Output");
239 pcmk__output_create_html_node(out, PCMK__XE_DIV, NULL, NULL, rc_buf);
240
241 if (proc_stdout != NULL) {
242 pcmk__output_create_html_node(out, PCMK__XE_DIV, NULL, NULL, "Stdout");
243 pcmk__output_create_html_node(out, PCMK__XE_DIV, NULL,
244 PCMK__VALUE_OUTPUT, proc_stdout);
245 }
246 if (proc_stderr != NULL) {
247 pcmk__output_create_html_node(out, PCMK__XE_DIV, NULL, NULL, "Stderr");
248 pcmk__output_create_html_node(out, PCMK__XE_DIV, NULL,
249 PCMK__VALUE_OUTPUT, proc_stderr);
250 }
251
252 free(rc_buf);
253 }
254
255 static void
256 html_version(pcmk__output_t *out)
257 {
258 pcmk__assert(out != NULL);
259
260 pcmk__output_create_xml_text_node(out, "h2", "Version Information");
261 pcmk__output_create_html_node(out, PCMK__XE_DIV, NULL, NULL,
262 "Program: Pacemaker");
263 pcmk__output_create_html_node(out, PCMK__XE_DIV, NULL, NULL,
264 "Version: " PACEMAKER_VERSION);
265 pcmk__output_create_html_node(out, PCMK__XE_DIV, NULL, NULL,
266 "Author: Andrew Beekhof and "
267 "the Pacemaker project contributors");
268 pcmk__output_create_html_node(out, PCMK__XE_DIV, NULL, NULL,
269 "Build: " BUILD_VERSION);
270 pcmk__output_create_html_node(out, PCMK__XE_DIV, NULL, NULL,
271 "Features: " CRM_FEATURES);
272 }
273
274 G_GNUC_PRINTF(2, 3)
275 static void
276 html_err(pcmk__output_t *out, const char *format, ...) {
277 private_data_t *priv = NULL;
278 int len = 0;
279 char *buf = NULL;
280 va_list ap;
281
282 pcmk__assert((out != NULL) && (out->priv != NULL));
283 priv = out->priv;
284
285 va_start(ap, format);
286 len = vasprintf(&buf, format, ap);
287 pcmk__assert(len >= 0);
288 va_end(ap);
289
290 priv->errors = g_slist_append(priv->errors, buf);
291 }
292
293 G_GNUC_PRINTF(2, 3)
294 static int
295 html_info(pcmk__output_t *out, const char *format, ...) {
296 return pcmk_rc_no_output;
297 }
298
299 static void
300 html_output_xml(pcmk__output_t *out, const char *name, const char *buf) {
301 htmlNodePtr node = NULL;
302
303 pcmk__assert(out != NULL);
304
305 node = pcmk__output_create_html_node(out, "pre", NULL, NULL, buf);
306 pcmk__xe_set(node, PCMK_XA_LANG, "xml");
307 }
308
309 G_GNUC_PRINTF(4, 5)
310 static void
311 html_begin_list(pcmk__output_t *out, const char *singular_noun,
312 const char *plural_noun, const char *format, ...) {
313 int q_len = 0;
314 private_data_t *priv = NULL;
315 xmlNodePtr node = NULL;
316
317 pcmk__assert((out != NULL) && (out->priv != NULL));
318 priv = out->priv;
319
320 /* If we are already in a list (the queue depth is always at least
321 * one because of the <html> element), first create a <li> element
322 * to hold the <h2> and the new list.
323 */
324 q_len = g_queue_get_length(priv->parent_q);
325 if (q_len > 2) {
326 pcmk__output_xml_create_parent(out, "li", NULL);
327 }
328
329 if (format != NULL) {
330 va_list ap;
331 char *buf = NULL;
332 int len;
333
334 va_start(ap, format);
335 len = vasprintf(&buf, format, ap);
336 va_end(ap);
337 pcmk__assert(len >= 0);
338
339 if (q_len > 2) {
340 pcmk__output_create_xml_text_node(out, "h3", buf);
341 } else {
342 pcmk__output_create_xml_text_node(out, "h2", buf);
343 }
344
345 free(buf);
346 }
347
348 node = pcmk__output_xml_create_parent(out, "ul", NULL);
349 g_queue_push_tail(priv->parent_q, node);
350 }
351
352 G_GNUC_PRINTF(3, 4)
353 static void
354 html_list_item(pcmk__output_t *out, const char *name, const char *format, ...) {
355 htmlNodePtr item_node = NULL;
356 va_list ap;
357 char *buf = NULL;
358 int len;
359
360 pcmk__assert(out != NULL);
361
362 va_start(ap, format);
363 len = vasprintf(&buf, format, ap);
364 pcmk__assert(len >= 0);
365 va_end(ap);
366
367 item_node = pcmk__output_create_xml_text_node(out, "li", buf);
368 free(buf);
369
370 if (name != NULL) {
371 pcmk__xe_set(item_node, PCMK_XA_CLASS, name);
372 }
373 }
374
375 static void
376 html_increment_list(pcmk__output_t *out) {
377 /* This function intentially left blank */
378 }
379
380 static void
381 html_end_list(pcmk__output_t *out) {
382 private_data_t *priv = NULL;
383
384 pcmk__assert((out != NULL) && (out->priv != NULL));
385 priv = out->priv;
386
387 /* Remove the <ul> tag, but do not free this result - it's still
388 * part of the document.
389 */
390 g_queue_pop_tail(priv->parent_q);
391 pcmk__output_xml_pop_parent(out);
392
393 /* Remove the <li> created for nested lists. */
394 if (g_queue_get_length(priv->parent_q) > 2) {
395 pcmk__output_xml_pop_parent(out);
396 }
397 }
398
399 static bool
400 html_is_quiet(pcmk__output_t *out) {
401 return false;
402 }
403
404 static void
405 html_spacer(pcmk__output_t *out) {
406 pcmk__assert(out != NULL);
407 pcmk__output_create_xml_node(out, "br", NULL);
408 }
409
410 static void
411 html_progress(pcmk__output_t *out, bool end) {
412 /* This function intentially left blank */
413 }
414
415 pcmk__output_t *
416 pcmk__mk_html_output(char **argv) {
417 pcmk__output_t *retval = calloc(1, sizeof(pcmk__output_t));
418
419 if (retval == NULL) {
420 return NULL;
421 }
422
423 retval->fmt_name = "html";
424 retval->request = pcmk__quote_cmdline(argv);
425
426 retval->init = html_init;
427 retval->free_priv = html_free_priv;
428 retval->finish = html_finish;
429 retval->reset = html_reset;
430
431 retval->register_message = pcmk__register_message;
432 retval->message = pcmk__call_message;
433
434 retval->subprocess_output = html_subprocess_output;
435 retval->version = html_version;
436 retval->info = html_info;
437 retval->transient = html_info;
438 retval->err = html_err;
439 retval->output_xml = html_output_xml;
440
441 retval->begin_list = html_begin_list;
442 retval->list_item = html_list_item;
443 retval->increment_list = html_increment_list;
444 retval->end_list = html_end_list;
445
446 retval->is_quiet = html_is_quiet;
447 retval->spacer = html_spacer;
448 retval->progress = html_progress;
449 retval->prompt = pcmk__text_prompt;
450
451 return retval;
452 }
453
454 xmlNodePtr
455 pcmk__output_create_html_node(pcmk__output_t *out, const char *element_name, const char *id,
456 const char *class_name, const char *text) {
457 htmlNodePtr node = NULL;
458
459 pcmk__assert(out != NULL);
460 CRM_CHECK(pcmk__str_eq(out->fmt_name, "html", pcmk__str_none), return NULL);
461
462 node = pcmk__output_create_xml_text_node(out, element_name, text);
463
464 if (class_name != NULL) {
465 pcmk__xe_set(node, PCMK_XA_CLASS, class_name);
466 }
467
468 if (id != NULL) {
469 pcmk__xe_set(node, PCMK_XA_ID, id);
470 }
471
472 return node;
473 }
474
475 /*!
476 * \internal
477 * \brief Create a new HTML element under a given parent with ID and class
478 *
479 * \param[in,out] parent XML element that will be the new element's parent
480 * (\c NULL to create a new XML document with the new
481 * node as root)
482 * \param[in] name Name of new element
483 * \param[in] id CSS ID of new element (can be \c NULL)
484 * \param[in] class_name CSS class of new element (can be \c NULL)
485 *
486 * \return Newly created XML element (guaranteed not to be \c NULL)
487 */
488 xmlNode *
489 pcmk__html_create(xmlNode *parent, const char *name, const char *id,
490 const char *class_name)
491 {
492 xmlNode *node = pcmk__xe_create(parent, name);
493
494 pcmk__xe_set_props(node,
495 PCMK_XA_CLASS, class_name,
496 PCMK_XA_ID, id,
497 NULL);
498 return node;
499 }
500
501 void
502 pcmk__html_set_title(const char *name)
503 {
504 g_free(title);
505 title = g_strdup(name);
506 }
507
508 void
509 pcmk__html_add_header(const char *name, ...) {
510 htmlNodePtr header_node;
511 va_list ap;
512
513 va_start(ap, name);
514
515 header_node = pcmk__xe_create(NULL, name);
516 while (1) {
517 char *key = va_arg(ap, char *);
518 char *value;
519
520 if (key == NULL) {
521 break;
522 }
523
524 value = va_arg(ap, char *);
525 pcmk__xe_set(header_node, key, value);
526 }
527
528 extra_headers = g_slist_append(extra_headers, header_node);
529
530 va_end(ap);
531 }
532