// SPDX-License-Identifier: GPL-2.0-only
/*
 * Copyright (C) 2009 Red Hat Inc.
 *
 * David Sommerseth <davids@redhat.com>
 *
 * Parses summary.xml reports from rteval into a standardised XML format
 * which is useful when putting data into a database.
 *
 */

/**
 * @file   xmlparser.c
 * @author David Sommerseth <davids@redhat.com>
 * @date   Wed Oct 21 10:58:53 2009
 *
 * @brief Parses summary.xml reports from rteval into a standardised XML format
 *        which is useful when putting data into a database.
 *
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>

#include <libxml/tree.h>
#include <libxslt/xsltInternals.h>
#include <libxslt/transform.h>
#include <libxslt/xsltutils.h>
#include <libexslt/exslt.h>

#include <eurephia_nullsafe.h>
#include <eurephia_xml.h>
#include <xmlparser.h>
#include <sha1.h>
#include <log.h>

static dbhelper_func const * xmlparser_dbhelpers = NULL;

/**
 * Simple strdup() function which encapsulates the string in single quotes,
 * which is needed for XSLT parameter values
 *
 * @param str The string to be strdup'ed and encapsulated
 *
 * @return Returns a pointer to the new buffer.
 */
static char *encapsString(const char *str) {
        char *ret = NULL;

        if( str == NULL ) {
                return NULL;
        }

        ret = (char *) calloc(1, strlen(str)+4);
        assert( ret != NULL );

        snprintf(ret, strlen(str)+3, "'%s'", str);
        return ret;
}


/**
 * Converts an integer to string an encapsulates the value in single quotes,
 * which is needed for XSLT parameter values.
 *
 * @param val Integer value to encapsulate
 *
 * @return Returns a pointer to a new buffer with the encapsulated integer value.  This
 *         buffer must be free'd after usage.
 */
static char *encapsInt(const unsigned int val) {
        char *buf = NULL;

        buf = (char *) calloc(1, 130);
        snprintf(buf, 128, "'%i'", val);
        return buf;
}


/**
 * Simple function to determine if the given string is a number or not
 *
 * @param str Pointer to the tring to be checked
 *
 * @returns Returns 0 if not a number and a non-null value if it is a number
 */
int isNumber(const char * str)
{
    char *ptr = NULL;

    if (str == NULL || *str == '\0' || isspace(*str))
      return 0;

    strtod (str, &ptr);
    return *ptr == '\0';
}


/**
 * Split a string into an array.
 * Use strGet() to extract the results and strFree() to free the memory
 * allocated by strSplit()
 *
 * @param str Input string
 * @param sep Separator list (string)
 *
 * @returns Returns a pointer to a array_str_t struct on success, otherwise NULL
 */
array_str_t * strSplit(const char * str, const char * sep)
{
        array_str_t * ret = calloc(1, sizeof(array_str_t));
        char * p = NULL, *cp = strdup(str);

        if( !ret || !cp ) {
                fprintf(stderr, "Memory allocation error in strSplit()\n");
                return NULL;
        }

        p = strtok(cp, sep);
        while( p ) {
                ret->data = realloc(ret->data, sizeof(char *) * ++(ret->size));
                if( !ret->data ) {
                        fprintf(stderr, "Memory allocation error in strSplit() while parsing\n");
                        free(ret);
                        return NULL;
                }
                ret->data[ret->size-1] = strdup(p);
                p = strtok(NULL, sep);
        }
        free(cp);
        return ret;
}

/**
 * Retrieve an array memeber
 *
 * @param ar Pointer holding the array_str_t data
 * @param el Element number to retrive
 *
 * @returns Returns a pointer to the array member.  This value must never be freed.
 * On failure NULL is returned.  Only possible failures are out-of-range or a NULL
 * array input.
 */
inline char * strGet(array_str_t * ar, unsigned int el)
{
        return (!ar && (el > ar->size) ? NULL : ar->data[el]);
}

/**
 * Retrive number of accessible array members
 *
 * @param ar Pointer holding the array_str_t data
 *
 * @returns Returns the number of elements in the array_str_t struct.
 *          If a NULL pointer is given, it will return 0.
 */
inline unsigned int strSize(array_str_t * ar)
{
        return (!ar ? 0 : ar->size);
}

/**
 * Frees up the memory held by an array_str_t struct.
 *
 * @params ar Pointer holding the array_str_t data to be freed
 *
 */
void strFree(array_str_t * ar)
{
        int i = 0;

        if( !ar ) {
                return;
        }

        for( i = 0; i < ar->size; i++ ) {
                free(ar->data[i]);
                ar->data[i] = NULL;
        }
        free(ar->data); ar->data = NULL;
        free(ar);
}

/**
 * Initialise the XML parser, setting some global variables
 */
void init_xmlparser(dbhelper_func const * dbhelpers)
{
	xmlparser_dbhelpers = dbhelpers;

        /* Init libxml2 and load all exslt functions */
        xmlInitMemory();
        exsltRegisterAll();
}


/**
 * Parses any XML input document into a sqldata XML format which can be used by pgsql_INSERT().
 * The transformation must be defined in the input XSLT template.
 *
 * @param log       Log context
 * @param xslt      XSLT template defining the data transformation
 * @param indata_d  Input XML data to transform to a sqldata XML document
 * @param params    Parameters to be sent to the XSLT parser
 *
 * @return Returns a well formed sqldata XML document on success, otherwise NULL is returned.
 */
xmlDoc *parseToSQLdata(LogContext *log, xsltStylesheet *xslt, xmlDoc *indata_d, parseParams *params) {
        xmlDoc *result_d = NULL;
        char **xsltparams = NULL;
        unsigned int idx = 0, idx_table = 0, idx_submid = 0,
		idx_syskey = 0, idx_rterid = 0, idx_repfname = 0;

        xsltparams = calloc(10, sizeof(char *));

        if( xmlparser_dbhelpers == NULL ) {
                writelog(log, LOG_ERR, "Programming error: xmlparser is not initialised");
                return NULL;
        }

        if( params->table == NULL ) {
                writelog(log, LOG_ERR, "Table is not defined");
                return NULL;
        }

        // Prepare XSLT parameters
        xsltparams[idx++] = "table\0";
        xsltparams[idx] = (char *) encapsString(params->table);
        idx_table = idx++;

        if( params->submid > 0) {
                xsltparams[idx++] = "submid\0";
                xsltparams[idx] = (char *) encapsInt(params->submid);
                idx_submid = idx++;
        }

        if( params->syskey > 0) {
                xsltparams[idx++] = "syskey\0";
                xsltparams[idx] = (char *) encapsInt(params->syskey);
                idx_syskey = idx++;
        }

        if( params->rterid > 0 ) {
                xsltparams[idx++] = "rterid";
                xsltparams[idx] = (char *) encapsInt(params->rterid);
                idx_rterid = idx++;
        }

        if( params->report_filename ) {
                xsltparams[idx++] = "report_filename";
                xsltparams[idx] = (char *) encapsString(params->report_filename);
                idx_repfname = idx++;
        }
        xsltparams[idx] = NULL;

        // Apply the XSLT template to the input XML data
        result_d = xsltApplyStylesheet(xslt, indata_d, (const char **)xsltparams);
        if( result_d == NULL ) {
                writelog(log, LOG_CRIT, "Failed applying XSLT template to input XML");
        }

        // Free memory we allocated via encapsString()/encapsInt()
        free(xsltparams[idx_table]);
        if( params->submid ) {
                free(xsltparams[idx_submid]);
        }
        if( params->syskey ) {
                free(xsltparams[idx_syskey]);
        }
        if( params->rterid ) {
                free(xsltparams[idx_rterid]);
        }
        if( params->report_filename ) {
                free(xsltparams[idx_repfname]);
        }

        free(xsltparams);
        return result_d;
}


/**
 * Internal xmlparser function.   Extracts the value from a '//sqldata/records/record/value'
 * node and hashes the value if the 'hash' attribute is set.  Otherwise the value is extracted
 * from the node directly.  This function is only used by sqldataExtractContent().
 *
 * @param sql_n sqldata values node containing the value to extract.
 *
 * @return Returns a pointer to a new buffer containing the value on success, otherwise NULL.
 *         This memory buffer must be free'd after usage.
 */
char * sqldataValueHash(LogContext *log, xmlNode *sql_n) {
	const char *hash = NULL, *isnull = NULL;
	SHA1Context shactx;
	uint8_t shahash[SHA1_HASH_SIZE];
	char *ret = NULL, *ptr = NULL;
	int i;

	if( !(sql_n && (xmlStrcmp(sql_n->name, (xmlChar *) "value") == 0)
              && (xmlStrcmp(sql_n->parent->name, (xmlChar *) "record") == 0)
              || (xmlStrcmp(sql_n->parent->name, (xmlChar *) "value") == 0)) ) {
                return NULL;
	}

	isnull = xmlGetAttrValue(sql_n->properties, "isnull");
	if( isnull && (strcmp(isnull, "1") == 0) ) {
		return NULL;
	}

	hash = xmlGetAttrValue(sql_n->properties, "hash");
	if( !hash ) {
		// If no hash attribute is found, just use the raw data
		ret = strdup_nullsafe(xmlExtractContent(sql_n));
	} else if( strcasecmp(hash, "sha1") == 0 ) {
		const char *indata = xmlExtractContent(sql_n);
		// SHA1 hashing requested
		SHA1Init(&shactx);
		SHA1Update(&shactx, indata, strlen_nullsafe(indata));
		SHA1Final(&shactx, shahash);

		// "Convert" to a readable format
		ret = malloc_nullsafe(log, (SHA1_HASH_SIZE * 2) + 3);
		ptr = ret;
		for( i = 0; i < SHA1_HASH_SIZE; i++ ) {
			sprintf(ptr, "%02x", shahash[i]);
			ptr += 2;
		}
	} else {
		ret = strdup("<Unsupported hashing algorithm>");
	}

	return ret;
}


/**
 * Extract the content of a //sqldata/records/record/value[@type='array']/value node set
 * and format it in suitable array format for the database backend.
 *
 * @param log    Log context
 * @param sql_n sqldata values node containing the value to extract and format as an array.
 *
 * @return Returns a pointer to a new memory buffer containing the value as a string.
 *         On errors, NULL is returned.  This memory buffer must be free'd after usage.
 */
static char * sqldataValueArray(LogContext *log, xmlNode *sql_n)
{
	if( xmlparser_dbhelpers == NULL ) {
		writelog(log, LOG_ERR, "Programming error: xmlparser is not initialised");
		return NULL;
	}

	return xmlparser_dbhelpers->dbh_FormatArray(log, sql_n);
}


/**
 * Extract the content of a '//sqldata/records/record/value' node.  It will consider
 * both the 'hash' and 'type' attributes of the 'value' tag.
 *
 * @param log    Log context
 * @param sql_n  Pointer to a value node of a sqldata XML document.
 *
 * @return Returns a pointer to a new memory buffer containing the value as a string.
 *         On errors, NULL is returned.  This memory buffer must be free'd after usage.
 */
char *sqldataExtractContent(LogContext *log, xmlNode *sql_n) {
	const char *valtype = xmlGetAttrValue(sql_n->properties, "type");

        if( xmlparser_dbhelpers == NULL ) {
                writelog(log, LOG_ERR, "Programming error: xmlparser is not initialised");
                return NULL;
        }

	if( !sql_n || (xmlStrcmp(sql_n->name, (xmlChar *) "value") != 0)
	    || (xmlStrcmp(sql_n->parent->name, (xmlChar *) "record") != 0) ) {
		    return NULL;
	}

	if( valtype && (strcmp(valtype, "xmlblob") == 0) ) {
		xmlNode *chld_n = sql_n->children;

		// Go to next "real" tag, skipping non-element nodes
		while( chld_n && chld_n->type != XML_ELEMENT_NODE ){
			chld_n = chld_n->next;
		}
		return xmlNodeToString(log, chld_n);
        } else if( valtype && (strcmp(valtype, "array") == 0) ) {
                return sqldataValueArray(log, sql_n);
	} else {
		return sqldataValueHash(log, sql_n);
	}
}


/**
 * Return the 'fid' value of a given field in an sqldata XML document.
 *
 * @param log    Log context
 * @param sql_n  Pointer to the root xmlNode element of a sqldata XML document
 * @param fname  String containing the field name to look up
 *
 * @return Returns a value >= 0 on success, containing the 'fid' value of the field.  Otherwise
 *         a value < 0 is returned.  -1 if the field is not found or -2 if there are some problems
 *         with the XML document.
 */
int sqldataGetFid(LogContext *log, xmlNode *sql_n, const char *fname) {
	xmlNode *f_n = NULL;

        if( xmlparser_dbhelpers == NULL ) {
                writelog(log, LOG_ERR, "Programming error: xmlparser is not initialised");
                return -2;
        }

	if( !sql_n || (xmlStrcmp(sql_n->name, (xmlChar *) "sqldata") != 0) ) {
		writelog(log, LOG_ERR,
			 "sqldataGetFid: Input XML document is not a valid sqldata document");
		return -2;
	}

	f_n = xmlFindNode(sql_n, "fields");
	if( !f_n || !f_n->children ) {
		writelog(log, LOG_ERR,
			 "sqldataGetFid: Input XML document does not contain a fields section");
		return -2;
	}

	foreach_xmlnode(f_n->children, f_n) {
		if( (f_n->type != XML_ELEMENT_NODE)
		    || xmlStrcmp(f_n->name, (xmlChar *) "field") != 0 ) {
			// Skip uninteresting nodes
			continue;
		}

		if( strcmp(xmlExtractContent(f_n), fname) == 0 ) {
			char *fid = xmlGetAttrValue(f_n->properties, "fid");
			if( !fid ) {
				writelog(log, LOG_ERR,
					 "sqldataGetFid: Field node is missing 'fid' attribute (field: %s)",
					 fname);
				return -2;
			}
			return atoi_nullsafe(fid);
		}
	}
	return -1;
}


/**
 * Retrieves the value of a particular field in an sqldata XML document.
 *
 * @param log    Log context
 * @param sqld   pointer to an sqldata XML document.
 * @param fname  String containing the field name to extract the value of.
 * @param recid  Integer containing the record ID of the record to extract the value.  This starts
 *               on 0.
 *
 * @return Returns a pointer to a new memory buffer containing the extracted value.  On errors or if
 *         recid is higher than available records, NULL is returned.
 */
char *sqldataGetValue(LogContext *log, xmlDoc *sqld, const char *fname, int recid ) {
	xmlNode *r_n = NULL;
	int fid = -3, rc = 0;

        if( xmlparser_dbhelpers == NULL ) {
                writelog(log, LOG_ERR, "Programming error: xmlparser is not initialised");
                return NULL;
        }

	if( recid < 0 ) {
		writelog(log, LOG_ERR, "sqldataGetValue: Invalid recid");
		return NULL;
	}

	r_n = xmlDocGetRootElement(sqld);
	if( !r_n || (xmlStrcmp(r_n->name, (xmlChar *) "sqldata") != 0) ) {
		writelog(log, LOG_ERR,
			 "sqldataGetValue: Input XML document is not a valid sqldata document");
		return NULL;
	}

	fid = sqldataGetFid(log, r_n, fname);
	if( fid < 0 ) {
		return NULL;
	}

	r_n = xmlFindNode(r_n, "records");
	if( !r_n || !r_n->children ) {
		writelog(log, LOG_ERR,
			 "sqldataGetValue: Input XML document does not contain a records section");
		return NULL;
	}

	foreach_xmlnode(r_n->children, r_n) {
		if( (r_n->type != XML_ELEMENT_NODE)
		    || xmlStrcmp(r_n->name, (xmlChar *) "record") != 0 ) {
			// Skip uninteresting nodes
			continue;
		}
		if( rc == recid ) {
			xmlNode *v_n = NULL;
			// The rigth record is found, find the field we're looking for
			foreach_xmlnode(r_n->children, v_n) {
				char *fid_s = NULL;
				if( (v_n->type != XML_ELEMENT_NODE)
				    || (xmlStrcmp(v_n->name, (xmlChar *) "value") != 0) ) {
					// Skip uninteresting nodes
					continue;
				}
				fid_s = xmlGetAttrValue(v_n->properties, "fid");
				if( fid_s && (fid == atoi_nullsafe(fid_s)) ) {
					return sqldataExtractContent(log, v_n);
				}
			}
		}
		rc++;
	}
	return NULL;
}


/**
 * Helper function to parse an sqldata XML document for the systems_hostname table.  In addition
 * it will also return two strings containing hostname and ipaddress of the host.
 *
 * @param log        Log context
 * @param xslt       Pointer to an xmlparser.xml XSLT template
 * @param summaryxml rteval XML report document
 * @param syskey     Integer containing the syskey value corresponding to this host
 * @param hostname   Return pointer for where the hostname will be saved.
 * @param ipaddr     Return pointer for where the IP address will be saved.
 *
 * @return Returns a sqldata XML document on success.  In this case the hostname and ipaddr will point
 *         at memory buffers containing hostname and ipaddress.  These values must be free'd after usage.
 *         On errors the function will return NULL and hostname and ipaddr will not have been touched
 *         at all.
 */
xmlDoc *sqldataGetHostInfo(LogContext *log, xsltStylesheet *xslt, xmlDoc *summaryxml,
			   int syskey, char **hostname, char **ipaddr)
{
	xmlDoc *hostinfo_d = NULL;
	parseParams prms;

        if( xmlparser_dbhelpers == NULL ) {
                writelog(log, LOG_ERR, "Programming error: xmlparser is not initialised");
                return NULL;
        }

	memset(&prms, 0, sizeof(parseParams));
	prms.table = "systems_hostname";
	prms.syskey = syskey;

	hostinfo_d = parseToSQLdata(log, xslt, summaryxml, &prms);
	if( !hostinfo_d ) {
		writelog(log, LOG_ERR,
			 "sqldatGetHostInfo: Could not parse input XML data");
		xmlFreeDoc(hostinfo_d);
		goto exit;
	}

	// Grab hostname from input XML
	*hostname = sqldataGetValue(log, hostinfo_d, "hostname", 0);
	if( !hostname ) {
		writelog(log, LOG_ERR,
			"sqldatGetHostInfo: Could not retrieve the hostname field from the input XML");
		xmlFreeDoc(hostinfo_d);
		goto exit;
	}

	// Grab ipaddr from input XML
	*ipaddr = sqldataGetValue(log, hostinfo_d, "ipaddr", 0);
	if( !ipaddr ) {
		writelog(log, LOG_ERR,
			"sqldatGetHostInfo: Could not retrieve the IP address field from the input XML");
		free_nullsafe(hostname);
		xmlFreeDoc(hostinfo_d);
		goto exit;
	}
 exit:
	return hostinfo_d;
}

int sqldataGetRequiredSchemaVer(LogContext *log, xmlNode *sqldata_root)
{
	char *schver = NULL, *cp = NULL, *ptr = NULL;
	int majv = 0, minv = 0;

        if( xmlparser_dbhelpers == NULL ) {
                writelog(log, LOG_ERR, "Programming error: xmlparser is not initialised");
                return -1;
        }

	if( !sqldata_root || (xmlStrcmp(sqldata_root->name, (xmlChar *) "sqldata") != 0) ) {
		writelog(log, LOG_ERR, "sqldataGetRequiredSchemaVer: Invalid document node");
		return -1;
	}

	schver = xmlGetAttrValue(sqldata_root->properties, "schemaver");
	if( schver == NULL ) {
		return 100;  // If not defined, presume lowest available version.
	}
	cp = strdup(schver);
	assert( cp != NULL );

	if( (ptr = strpbrk(cp, ".")) != NULL ) {
		*ptr = 0;
		ptr++;
		majv = atoi_nullsafe(cp);
		minv = atoi_nullsafe(ptr);
	} else {
		majv = atoi_nullsafe(cp);
		minv = 0;
	}
	free_nullsafe(cp);

	return (majv * 100) + minv;
}
