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