summaryrefslogtreecommitdiffstats
path: root/lib/common/output_html.c
diff options
context:
space:
mode:
Diffstat (limited to 'lib/common/output_html.c')
-rw-r--r--lib/common/output_html.c477
1 files changed, 477 insertions, 0 deletions
diff --git a/lib/common/output_html.c b/lib/common/output_html.c
new file mode 100644
index 0000000..47b14c1
--- /dev/null
+++ b/lib/common/output_html.c
@@ -0,0 +1,477 @@
+/*
+ * Copyright 2019-2022 the Pacemaker project contributors
+ *
+ * The version control history for this file may have further details.
+ *
+ * This source code is licensed under the GNU Lesser General Public License
+ * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
+ */
+
+#include <crm_internal.h>
+
+#include <ctype.h>
+#include <libxml/HTMLtree.h>
+#include <stdarg.h>
+#include <stdlib.h>
+#include <stdio.h>
+
+#include <crm/common/cmdline_internal.h>
+#include <crm/common/xml.h>
+
+static const char *stylesheet_default =
+ ".bold { font-weight: bold }\n"
+
+ ".online { color: green }\n"
+ ".offline { color: red }\n"
+ ".maint { color: blue }\n"
+ ".standby { color: blue }\n"
+ ".health_red { color: red }\n"
+ ".health_yellow { color: GoldenRod }\n"
+
+ ".rsc-failed { color: red }\n"
+ ".rsc-failure-ignored { color: DarkGreen }\n"
+ ".rsc-managed { color: blue }\n"
+ ".rsc-multiple { color: orange }\n"
+ ".rsc-ok { color: green }\n"
+
+ ".warning { color: red; font-weight: bold }";
+
+static gboolean cgi_output = FALSE;
+static char *stylesheet_link = NULL;
+static char *title = NULL;
+static GSList *extra_headers = NULL;
+
+GOptionEntry pcmk__html_output_entries[] = {
+ { "html-cgi", 0, 0, G_OPTION_ARG_NONE, &cgi_output,
+ "Add CGI headers (requires --output-as=html)",
+ NULL },
+
+ { "html-stylesheet", 0, 0, G_OPTION_ARG_STRING, &stylesheet_link,
+ "Link to an external stylesheet (requires --output-as=html)",
+ "URI" },
+
+ { "html-title", 0, 0, G_OPTION_ARG_STRING, &title,
+ "Specify a page title (requires --output-as=html)",
+ "TITLE" },
+
+ { NULL }
+};
+
+/* The first several elements of this struct must be the same as the first
+ * several elements of private_data_s in lib/common/output_xml.c. This
+ * struct gets passed to a bunch of the pcmk__output_xml_* functions which
+ * assume an XML private_data_s. Keeping them laid out the same means this
+ * still works.
+ */
+typedef struct private_data_s {
+ /* Begin members that must match the XML version */
+ xmlNode *root;
+ GQueue *parent_q;
+ GSList *errors;
+ /* End members that must match the XML version */
+} private_data_t;
+
+static void
+html_free_priv(pcmk__output_t *out) {
+ private_data_t *priv = NULL;
+
+ if (out == NULL || out->priv == NULL) {
+ return;
+ }
+
+ priv = out->priv;
+
+ xmlFreeNode(priv->root);
+ g_queue_free(priv->parent_q);
+ g_slist_free(priv->errors);
+ free(priv);
+ out->priv = NULL;
+}
+
+static bool
+html_init(pcmk__output_t *out) {
+ private_data_t *priv = NULL;
+
+ CRM_ASSERT(out != NULL);
+
+ /* If html_init was previously called on this output struct, just return. */
+ if (out->priv != NULL) {
+ return true;
+ } else {
+ out->priv = calloc(1, sizeof(private_data_t));
+ if (out->priv == NULL) {
+ return false;
+ }
+
+ priv = out->priv;
+ }
+
+ priv->parent_q = g_queue_new();
+
+ priv->root = create_xml_node(NULL, "html");
+ xmlCreateIntSubset(priv->root->doc, (pcmkXmlStr) "html", NULL, NULL);
+
+ crm_xml_add(priv->root, "lang", "en");
+ g_queue_push_tail(priv->parent_q, priv->root);
+ priv->errors = NULL;
+
+ pcmk__output_xml_create_parent(out, "body", NULL);
+
+ return true;
+}
+
+static void
+add_error_node(gpointer data, gpointer user_data) {
+ char *str = (char *) data;
+ pcmk__output_t *out = (pcmk__output_t *) user_data;
+ out->list_item(out, NULL, "%s", str);
+}
+
+static void
+html_finish(pcmk__output_t *out, crm_exit_t exit_status, bool print, void **copy_dest) {
+ private_data_t *priv = NULL;
+ htmlNodePtr head_node = NULL;
+ htmlNodePtr charset_node = NULL;
+
+ CRM_ASSERT(out != NULL);
+
+ priv = out->priv;
+
+ /* If root is NULL, html_init failed and we are being called from pcmk__output_free
+ * in the pcmk__output_new path.
+ */
+ if (priv == NULL || priv->root == NULL) {
+ return;
+ }
+
+ if (cgi_output && print) {
+ fprintf(out->dest, "Content-Type: text/html\n\n");
+ }
+
+ /* Add the head node last - it's not needed earlier because it doesn't contain
+ * anything else that the user could add, and we want it done last to pick up
+ * any options that may have been given.
+ */
+ head_node = xmlNewNode(NULL, (pcmkXmlStr) "head");
+
+ if (title != NULL ) {
+ pcmk_create_xml_text_node(head_node, "title", title);
+ } else if (out->request != NULL) {
+ pcmk_create_xml_text_node(head_node, "title", out->request);
+ }
+
+ charset_node = create_xml_node(head_node, "meta");
+ crm_xml_add(charset_node, "charset", "utf-8");
+
+ /* Add any extra header nodes the caller might have created. */
+ for (int i = 0; i < g_slist_length(extra_headers); i++) {
+ xmlAddChild(head_node, xmlCopyNode(g_slist_nth_data(extra_headers, i), 1));
+ }
+
+ /* Stylesheets are included two different ways. The first is via a built-in
+ * default (see the stylesheet_default const above). The second is via the
+ * html-stylesheet option, and this should obviously be a link to a
+ * stylesheet. The second can override the first. At least one should be
+ * given.
+ */
+ pcmk_create_xml_text_node(head_node, "style", stylesheet_default);
+
+ if (stylesheet_link != NULL) {
+ htmlNodePtr link_node = create_xml_node(head_node, "link");
+ pcmk__xe_set_props(link_node, "rel", "stylesheet",
+ "href", stylesheet_link,
+ NULL);
+ }
+
+ xmlAddPrevSibling(priv->root->children, head_node);
+
+ if (g_slist_length(priv->errors) > 0) {
+ out->begin_list(out, "Errors", NULL, NULL);
+ g_slist_foreach(priv->errors, add_error_node, (gpointer) out);
+ out->end_list(out);
+ }
+
+ if (print) {
+ htmlDocDump(out->dest, priv->root->doc);
+ }
+
+ if (copy_dest != NULL) {
+ *copy_dest = copy_xml(priv->root);
+ }
+
+ g_slist_free_full(extra_headers, (GDestroyNotify) xmlFreeNode);
+ extra_headers = NULL;
+}
+
+static void
+html_reset(pcmk__output_t *out) {
+ CRM_ASSERT(out != NULL);
+
+ out->dest = freopen(NULL, "w", out->dest);
+ CRM_ASSERT(out->dest != NULL);
+
+ html_free_priv(out);
+ html_init(out);
+}
+
+static void
+html_subprocess_output(pcmk__output_t *out, int exit_status,
+ const char *proc_stdout, const char *proc_stderr) {
+ char *rc_buf = NULL;
+
+ CRM_ASSERT(out != NULL);
+
+ rc_buf = crm_strdup_printf("Return code: %d", exit_status);
+
+ pcmk__output_create_xml_text_node(out, "h2", "Command Output");
+ pcmk__output_create_html_node(out, "div", NULL, NULL, rc_buf);
+
+ if (proc_stdout != NULL) {
+ pcmk__output_create_html_node(out, "div", NULL, NULL, "Stdout");
+ pcmk__output_create_html_node(out, "div", NULL, "output", proc_stdout);
+ }
+ if (proc_stderr != NULL) {
+ pcmk__output_create_html_node(out, "div", NULL, NULL, "Stderr");
+ pcmk__output_create_html_node(out, "div", NULL, "output", proc_stderr);
+ }
+
+ free(rc_buf);
+}
+
+static void
+html_version(pcmk__output_t *out, bool extended) {
+ CRM_ASSERT(out != NULL);
+
+ pcmk__output_create_xml_text_node(out, "h2", "Version Information");
+ pcmk__output_create_html_node(out, "div", NULL, NULL, "Program: Pacemaker");
+ pcmk__output_create_html_node(out, "div", NULL, NULL, crm_strdup_printf("Version: %s", PACEMAKER_VERSION));
+ pcmk__output_create_html_node(out, "div", NULL, NULL,
+ "Author: Andrew Beekhof and "
+ "the Pacemaker project contributors");
+ pcmk__output_create_html_node(out, "div", NULL, NULL, crm_strdup_printf("Build: %s", BUILD_VERSION));
+ pcmk__output_create_html_node(out, "div", NULL, NULL, crm_strdup_printf("Features: %s", CRM_FEATURES));
+}
+
+G_GNUC_PRINTF(2, 3)
+static void
+html_err(pcmk__output_t *out, const char *format, ...) {
+ private_data_t *priv = NULL;
+ int len = 0;
+ char *buf = NULL;
+ va_list ap;
+
+ CRM_ASSERT(out != NULL && out->priv != NULL);
+ priv = out->priv;
+
+ va_start(ap, format);
+ len = vasprintf(&buf, format, ap);
+ CRM_ASSERT(len >= 0);
+ va_end(ap);
+
+ priv->errors = g_slist_append(priv->errors, buf);
+}
+
+G_GNUC_PRINTF(2, 3)
+static int
+html_info(pcmk__output_t *out, const char *format, ...) {
+ return pcmk_rc_no_output;
+}
+
+static void
+html_output_xml(pcmk__output_t *out, const char *name, const char *buf) {
+ htmlNodePtr node = NULL;
+
+ CRM_ASSERT(out != NULL);
+
+ node = pcmk__output_create_html_node(out, "pre", NULL, NULL, buf);
+ crm_xml_add(node, "lang", "xml");
+}
+
+G_GNUC_PRINTF(4, 5)
+static void
+html_begin_list(pcmk__output_t *out, const char *singular_noun,
+ const char *plural_noun, const char *format, ...) {
+ int q_len = 0;
+ private_data_t *priv = NULL;
+ xmlNodePtr node = NULL;
+
+ CRM_ASSERT(out != NULL && out->priv != NULL);
+ priv = out->priv;
+
+ /* If we are already in a list (the queue depth is always at least
+ * one because of the <html> element), first create a <li> element
+ * to hold the <h2> and the new list.
+ */
+ q_len = g_queue_get_length(priv->parent_q);
+ if (q_len > 2) {
+ pcmk__output_xml_create_parent(out, "li", NULL);
+ }
+
+ if (format != NULL) {
+ va_list ap;
+ char *buf = NULL;
+ int len;
+
+ va_start(ap, format);
+ len = vasprintf(&buf, format, ap);
+ va_end(ap);
+ CRM_ASSERT(len >= 0);
+
+ if (q_len > 2) {
+ pcmk__output_create_xml_text_node(out, "h3", buf);
+ } else {
+ pcmk__output_create_xml_text_node(out, "h2", buf);
+ }
+
+ free(buf);
+ }
+
+ node = pcmk__output_xml_create_parent(out, "ul", NULL);
+ g_queue_push_tail(priv->parent_q, node);
+}
+
+G_GNUC_PRINTF(3, 4)
+static void
+html_list_item(pcmk__output_t *out, const char *name, const char *format, ...) {
+ htmlNodePtr item_node = NULL;
+ va_list ap;
+ char *buf = NULL;
+ int len;
+
+ CRM_ASSERT(out != NULL);
+
+ va_start(ap, format);
+ len = vasprintf(&buf, format, ap);
+ CRM_ASSERT(len >= 0);
+ va_end(ap);
+
+ item_node = pcmk__output_create_xml_text_node(out, "li", buf);
+ free(buf);
+
+ if (name != NULL) {
+ crm_xml_add(item_node, "class", name);
+ }
+}
+
+static void
+html_increment_list(pcmk__output_t *out) {
+ /* This function intentially left blank */
+}
+
+static void
+html_end_list(pcmk__output_t *out) {
+ private_data_t *priv = NULL;
+
+ CRM_ASSERT(out != NULL && out->priv != NULL);
+ priv = out->priv;
+
+ /* Remove the <ul> tag. */
+ g_queue_pop_tail(priv->parent_q);
+ pcmk__output_xml_pop_parent(out);
+
+ /* Remove the <li> created for nested lists. */
+ if (g_queue_get_length(priv->parent_q) > 2) {
+ pcmk__output_xml_pop_parent(out);
+ }
+}
+
+static bool
+html_is_quiet(pcmk__output_t *out) {
+ return false;
+}
+
+static void
+html_spacer(pcmk__output_t *out) {
+ CRM_ASSERT(out != NULL);
+ pcmk__output_create_xml_node(out, "br", NULL);
+}
+
+static void
+html_progress(pcmk__output_t *out, bool end) {
+ /* This function intentially left blank */
+}
+
+pcmk__output_t *
+pcmk__mk_html_output(char **argv) {
+ pcmk__output_t *retval = calloc(1, sizeof(pcmk__output_t));
+
+ if (retval == NULL) {
+ return NULL;
+ }
+
+ retval->fmt_name = "html";
+ retval->request = pcmk__quote_cmdline(argv);
+
+ retval->init = html_init;
+ retval->free_priv = html_free_priv;
+ retval->finish = html_finish;
+ retval->reset = html_reset;
+
+ retval->register_message = pcmk__register_message;
+ retval->message = pcmk__call_message;
+
+ retval->subprocess_output = html_subprocess_output;
+ retval->version = html_version;
+ retval->info = html_info;
+ retval->transient = html_info;
+ retval->err = html_err;
+ retval->output_xml = html_output_xml;
+
+ retval->begin_list = html_begin_list;
+ retval->list_item = html_list_item;
+ retval->increment_list = html_increment_list;
+ retval->end_list = html_end_list;
+
+ retval->is_quiet = html_is_quiet;
+ retval->spacer = html_spacer;
+ retval->progress = html_progress;
+ retval->prompt = pcmk__text_prompt;
+
+ return retval;
+}
+
+xmlNodePtr
+pcmk__output_create_html_node(pcmk__output_t *out, const char *element_name, const char *id,
+ const char *class_name, const char *text) {
+ htmlNodePtr node = NULL;
+
+ CRM_ASSERT(out != NULL);
+ CRM_CHECK(pcmk__str_eq(out->fmt_name, "html", pcmk__str_none), return NULL);
+
+ node = pcmk__output_create_xml_text_node(out, element_name, text);
+
+ if (class_name != NULL) {
+ crm_xml_add(node, "class", class_name);
+ }
+
+ if (id != NULL) {
+ crm_xml_add(node, "id", id);
+ }
+
+ return node;
+}
+
+void
+pcmk__html_add_header(const char *name, ...) {
+ htmlNodePtr header_node;
+ va_list ap;
+
+ va_start(ap, name);
+
+ header_node = xmlNewNode(NULL, (pcmkXmlStr) name);
+ while (1) {
+ char *key = va_arg(ap, char *);
+ char *value;
+
+ if (key == NULL) {
+ break;
+ }
+
+ value = va_arg(ap, char *);
+ crm_xml_add(header_node, key, value);
+ }
+
+ extra_headers = g_slist_append(extra_headers, header_node);
+
+ va_end(ap);
+}