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