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