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