/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
//
// -proc.c
//
// specialized markup processors
//
// Copyright (c) 1995-96 Jim Nelson.  Permission to distribute
// granted by the author.  No warranties are made on the fitness of this
// source code.
//
*/

#include "defs.h"
#include "macro.h"

#include "htp-files.h"
#include "def-proc.h"
#include "file-proc.h"
#include "snprintf.h"

#ifdef HAVE_PIPE
#include <errno.h>
#include <sys/types.h>
#include <sys/wait.h>
#endif

uint ExternalFileProcessor(TASK *task, HTML_MARKUP *htmlMarkup,
                           const char *externalName, char **newPlaintext)
{
    struct stat fileStat;
    struct tm *fileTime;
    HTML_ATTRIBUTE *attrib;

    assert(externalName != NULL);
    assert(htmlMarkup != NULL);

    /* get information on the file itself */
    if(stat(externalName, &fileStat) != 0)
    {
        HtpMsg(MSG_ERROR, task->infile, "unable to retrieve file information on \"%s\"",
            externalName);
        return MARKUP_ERROR;
    }

    /* allocate room for the replacment plaintext */
    if((*newPlaintext = AllocMemory(MAX_TIME_DATE_SIZE)) == NULL)
    {
        HtpMsg(MSG_ERROR, task->infile, "unable to allocate memory for expansion");
        return MARKUP_ERROR;
    }

    /* create new plaintext depending on what extra information is specified */
    if((attrib = UnlinkAttributeInMarkup(htmlMarkup, "SIZE")) != NULL)
    {
        uint precision;
        HTML_ATTRIBUTE *pattrib;

        /* get the precision attribute value, if present */
        precision = DEFAULT_PRECISION;
        if ((pattrib = UnlinkAttributeInMarkup(htmlMarkup, "PRECISION")) != NULL)
        {
            if(pattrib->value != NULL)
            {
                precision = atoi(pattrib->value);
            }
            else
            {
                HtpMsg(MSG_WARNING, task->infile, 
                       "precision attribute needs a value");
            }
            DestroyAttribute(pattrib);
            FreeMemory(pattrib);
        }

        /* expand markup depending on how SIZE should be represented */
        if((attrib->value == NULL) || (stricmp(attrib->value, "BYTE") == 0))
        {
            /* byte representation is default */
            snprintf(*newPlaintext, MAX_TIME_DATE_SIZE, 
                     "%lu", fileStat.st_size);
        }
        else if(stricmp(attrib->value, "KBYTE") == 0)
        {
            snprintf(*newPlaintext, MAX_TIME_DATE_SIZE,
                     "%.*f", (int) precision,
                     (double) ((double) fileStat.st_size / (double) KBYTE));
        }
        else if(stricmp(attrib->value, "MBYTE") == 0)
        {
            snprintf(*newPlaintext, MAX_TIME_DATE_SIZE,
                     "%.*f", (int) precision,
                     (double) ((double) fileStat.st_size / (double) MBYTE));
        }
        else if(stricmp(attrib->value, "GBYTE") == 0)
        {
            snprintf(*newPlaintext, MAX_TIME_DATE_SIZE,
                     "%.*f", (int) precision,
                     (double) ((double) fileStat.st_size / (double) GBYTE));
        }
        else
        {
            /* free the plaintext memory before returning */
            HtpMsg(MSG_ERROR, task->infile, "unknown SIZE specifier");
            DestroyAttribute(attrib);
            FreeMemory(attrib);
            FreeMemory(*newPlaintext);
            *newPlaintext = NULL;
            return MARKUP_ERROR;
        }
        DestroyAttribute(attrib);
        FreeMemory(attrib);
    }
    else if((attrib = UnlinkAttributeInMarkup(htmlMarkup, "TIME")) != NULL)
    {
        const char *value;

        /* convert into an ANSI time structure */
        fileTime = localtime(&fileStat.st_mtime);

        /* see if the attribute has a value ... if so, let it be the */
        /* strftime() formatter */
        if((value = MarkupAttributeValue(htmlMarkup, "TIME")) != NULL)
        {
            strftime(*newPlaintext, MAX_TIME_DATE_SIZE, value, fileTime);
        }
        else
        {
            strftime(*newPlaintext, MAX_TIME_DATE_SIZE, "%I:%M:%S %p", fileTime);
        }
        DestroyAttribute(attrib);
        FreeMemory(attrib);
    }
    else if((attrib = UnlinkAttributeInMarkup(htmlMarkup, "DATE")) != NULL)
    {
        /* convert into an ANSI time structure */
        fileTime = localtime(&fileStat.st_mtime);

        /* see if the attribute has a value ... if so, let it be the */
        /* strftime() formatter */
        if (attrib->value != NULL)
        {
            strftime(*newPlaintext, MAX_TIME_DATE_SIZE, 
                     attrib->value, fileTime);
        }
        else
        {
            strftime(*newPlaintext, MAX_TIME_DATE_SIZE,
                     "%a %b %d, %Y", fileTime);
        }
        DestroyAttribute(attrib);
        FreeMemory(attrib);
    }
    else
    {
        /* free the plaintext, unused */
        FreeMemory(*newPlaintext);
        *newPlaintext = NULL;

        HtpMsg(MSG_ERROR, task->infile, "invalid FILE tag");
        return MARKUP_ERROR;
    }

    /* the new plaintext was created successfully */
    return NEW_MARKUP;
}

uint FileExecuteProcessor(TASK *task, 
                          HTML_ATTRIBUTE *attrib, HTML_MARKUP *htmlMarkup)
{
    const char *cmdline;
    const char *output;
    HTML_ATTRIBUTE *outputAttrib;
#ifndef HAVE_PIPE
    char newCmdline[MAX_CMDLINE_LEN];
    char tempFilename[MAX_PATHNAME_LEN];
#endif
    BOOL redirect, ignoreError;
    STREAM infile;
    TASK newTask;
    BOOL result = TRUE;
    int exitCode;

    assert(task != NULL);
    assert(htmlMarkup != NULL);

    cmdline = attrib->value;

    redirect = UnlinkBoolAttributeInMarkup(htmlMarkup, "REDIRECT");
    ignoreError = UnlinkBoolAttributeInMarkup(htmlMarkup, "NOERROR");
    outputAttrib = UnlinkAttributeInMarkup(htmlMarkup, "OUTPUT");
    if (outputAttrib != NULL)
        output = outputAttrib->value;
    else
        output = NULL;

    /* either output or redirect, but not both, must be specified */
    if((output == NULL) && (redirect == FALSE))
    {
        HtpMsg(MSG_ERROR, task->infile, "Either REDIRECT or OUTPUT must be specified for FILE EXECUTE");
        return MARKUP_ERROR;
    }

    if((output != NULL) && (redirect == TRUE))
    {
        HtpMsg(MSG_ERROR, task->infile, "REDIRECT and OUTPUT cannot both be specified for FILE EXECUTE");
        DestroyAttribute(outputAttrib);
        FreeMemory(outputAttrib);
        return MARKUP_ERROR;
    }

    HtpMsg(MSG_INFO, task->infile, "Executing command \"%s\" ...", cmdline);

#ifndef HAVE_PIPE
    /* if redirection required, append to the command-line a redirector to */
    /* a temporary file */
    if(redirect)
    {
        if(CreateTempFilename(tempFilename, MAX_PATHNAME_LEN) == FALSE)
        {
            HtpMsg(MSG_ERROR, task->infile, "unable to create a temporary file for redirection");
            return MARKUP_ERROR;
        }

        strncpy(newCmdline, cmdline, MAX_PATHNAME_LEN);
        strncat(newCmdline, " > ", MAX_PATHNAME_LEN);
        strncat(newCmdline, tempFilename, MAX_PATHNAME_LEN);
        cmdline = newCmdline;
        output = tempFilename;
    }

    /* execute the command */
    exitCode = system(cmdline);

#else
    /* if redirection required, create a pipe for our child process */
    if (redirect) {
        pid_t pid;
        int pipefds[2];
        extern char **environ;

        if (pipe(pipefds) != 0)
        {
            HtpMsg(MSG_ERROR, task->infile, "unable to create pipe for redirection");
            return MARKUP_ERROR;
        }

        pid = fork();
        if (pid == 0) {
            char *argv[4];

            dup2(pipefds[1], 1);
            close(pipefds[0]);
            close(pipefds[1]);
            
            argv[0] = "sh";
            argv[1] = "-c";
            argv[2] = (char*)cmdline;
            argv[3] = NULL;
            execve("/bin/sh", argv, environ);
            exit(127);
        }

        close(pipefds[1]);

        if (pid == -1) 
            exitCode = -1;
        else {
            if (CreateFDReader(&infile, "stdout", pipefds[0]) == FALSE)
            {
                HtpMsg(MSG_ERROR, task->infile, "unable to open execute result file");
                return MARKUP_ERROR;
            }
            
            /* build a new task */
            newTask.infile = &infile;
            newTask.outfile = task->outfile;
            newTask.varstore = task->varstore;
            newTask.sourceFilename = task->sourceFilename;
            
            /* process the file */
            result = ProcessTask(&newTask);

            if (!result) {
                /* Error message was already spitted out.  However, we
                 * should give a hint where the meta-tag was called.  
                 */
                HtpMsg(MSG_ERROR, task->infile,
                       "... in output from '%s'", cmdline);
            }

            CloseStream(&infile);

            /* wait for termination of the command */
            while (waitpid(pid, &exitCode, 0) == -1) {
                if (errno != EINTR) {
                    exitCode = -1;
                    break;
                }
            }
        }
    } else {
        /* execute the command */
        exitCode = system(cmdline);
    }

#endif

    if(exitCode != 0 && ignoreError == FALSE)
    {
        /* the program has exited with an error condition */
        HtpMsg(MSG_ERROR, task->infile, "Command \"%s\" exited with an error code of %u",
               cmdline, exitCode);
        
        /* remove the temporary file, in case it was partially created */
        if (output != NULL)
            remove(output);
        
        if (outputAttrib != NULL) {
            DestroyAttribute(outputAttrib);
            FreeMemory(outputAttrib);
        }
        return MARKUP_ERROR;
    }

    if (output != NULL)
    {
        /* include the output file like it was anything else */
        /* first, open it */
        if(CreateFileReader(&infile, output) == FALSE)
        {
            HtpMsg(MSG_ERROR, task->infile, "unable to open execute result file");
            return MARKUP_ERROR;
        }
        
        /* build a new task */
        newTask.infile = &infile;
        newTask.outfile = task->outfile;
        newTask.varstore = task->varstore;
        newTask.sourceFilename = task->sourceFilename;
        
        /* process the file */
        result = ProcessTask(&newTask);

        if (!result) {
            /* Error message was already spitted out.  However, we
             * should give a hint where the meta-tag was called.  
             */
            HtpMsg(MSG_ERROR, task->infile,
                   "... in output from '%s'", cmdline);
        }

        /* close and destroy the output file */
        CloseStream(&infile);
        remove(output);

        if (outputAttrib != NULL) {
            DestroyAttribute(outputAttrib);
            FreeMemory(outputAttrib);
        }
    }
    return (result == TRUE) ? DISCARD_MARKUP : MARKUP_ERROR;
}

uint FileTemplateProcessor(TASK *task, HTML_ATTRIBUTE *attrib)
{
    assert(task != NULL);
    assert(attrib != NULL);
    
    /* the template file is not actually processed now, but rather
     * when the rest of the file is completed ... to postpone
     * processing, the template name is kept in the variable store
     * under a special name and retrieved later
     */
    if(StoreVariable(task->varstore, VAR_TEMPLATE_NAME, attrib->value,
                     VAR_TYPE_INTERNAL, VAR_FLAG_DEALLOC_VALUE, 
                         NULL, NULL) == FALSE)
    {
        HtpMsg(MSG_ERROR, task->infile,
               "unable to store template filename for post-processing (out of memory?)");
            return MARKUP_ERROR;
    }

    /* attrib->value was moved into VARSTORE */
    attrib->value = NULL;

    return DISCARD_MARKUP;
}

uint FileIncludeProcessor(TASK *task, 
                          const char *filename, HTML_MARKUP *htmlMarkup)
{
    STREAM incfile;
    BOOL result;
    TASK newTask;
    char fullPathname[MAX_PATHNAME_LEN];
    VARSTORE varstore;
    VARSTORE *topVarstore;
    uint flag;

    /* open the include file as input */
    if(CreateFileReader(&incfile, filename) == FALSE)
    {
        /* use the search path to find the file */
        if(SearchForFile(filename, fullPathname, MAX_PATHNAME_LEN) == FALSE)
        {
            /* could not find the file in the search path either */
            HtpMsg(MSG_ERROR, task->infile, "unable to open include file \"%s\"",
                filename);
            return MARKUP_ERROR;
        }

        if(CreateFileReader(&incfile, fullPathname) == FALSE)
        {
            HtpMsg(MSG_ERROR, task->infile, "unable to open include file \"%s\"",
                fullPathname);
            return MARKUP_ERROR;
        }
    }
    else
    {
        StringCopy(fullPathname, filename, MAX_PATHNAME_LEN);
    }

    /* if additional parameters exist in the tag, build a local varstore */
    /* and push it onto the context */
    if (htmlMarkup->attrib != NULL)
    {
        HTML_ATTRIBUTE *attrib;
        if(InitializeVariableStore(&varstore) == FALSE)
        {
            HtpMsg(MSG_ERROR, task->infile, "unable to initialize context for include file");
            CloseStream(&incfile);
            return MARKUP_ERROR;
        }

        /* start including the parameters as local macros */
        while (htmlMarkup->attrib != NULL)
        {
            attrib = htmlMarkup->attrib;

            /* varstore inherits the name and value from attribute. */
            flag = VAR_FLAG_DEALLOC_NAME | VAR_FLAG_DEALLOC_VALUE;
            if (!attrib->value) {
                attrib->value = "";
                flag &= ~VAR_FLAG_DEALLOC_VALUE;
            }

            if (StoreVariable(&varstore, attrib->name, attrib->value, 
                              VAR_TYPE_SET_MACRO,
                              flag | VAR_FLAG_DEALLOC_NAME | VAR_FLAG_DEALLOC_VALUE,
                              NULL, NULL) == FALSE)
            {
                HtpMsg(MSG_ERROR, task->infile, 
                       "unable to add variable to context for include file");
                CloseStream(&incfile);
                DestroyVariableStore(&varstore);
                return MARKUP_ERROR;
            }
            if (attrib->whitespace)
                FreeMemory(attrib->whitespace);

            /* unlink attrib */
            htmlMarkup->attrib = attrib->next;
            FreeMemory(attrib);
        }

        /* push this onto the context and use it for the include file */
        PushVariableStoreContext(task->varstore, &varstore);
        topVarstore = &varstore;
    }
    else
    {
        topVarstore = task->varstore;
    }

    /* build a new task structure */
    newTask.infile = &incfile;
    newTask.outfile = task->outfile;
    newTask.varstore = topVarstore;
    newTask.sourceFilename = task->sourceFilename;

    /* informational message for the user */
    HtpMsg(MSG_INFO, task->infile, "including file \"%s\"", fullPathname);

    /* process the new input file */
    result = ProcessTask(&newTask);
    if (!result) {
        /* Error message was already spitted out.  However, we
         * should give a hint where the meta-tag was called.  
         */
        HtpMsg(MSG_ERROR, task->infile, "... in file included here");
    }

    /* pop the local context */
    if(topVarstore == &varstore)
    {
        assert(topVarstore->child == NULL);
        PopVariableStoreContext(topVarstore);
        DestroyVariableStore(&varstore);
    }

    CloseStream(&incfile);

    /* if the new file did not process, return an error, otherwise discard */
    /* the markup */
    return (result == TRUE) ? DISCARD_MARKUP : MARKUP_ERROR;
}

uint FileProcessor(TASK *task, HTML_MARKUP *htmlMarkup, char **newPlaintext)
{
    HTML_ATTRIBUTE *attrib;
    BOOL result;

    if ((attrib = UnlinkAttributeInMarkup(htmlMarkup, "EXECUTE")) != NULL)
    {
        result = FileExecuteProcessor(task, attrib, htmlMarkup);
        DestroyAttribute(attrib);
        FreeMemory(attrib);
    } 
    else if ((attrib = UnlinkAttributeInMarkup(htmlMarkup, "TEMPLATE")) != NULL)
    {
        result = FileTemplateProcessor(task, attrib);
        DestroyAttribute(attrib);
        FreeMemory(attrib);
    }

    else if ((attrib = UnlinkAttributeInMarkup(htmlMarkup, "INCLUDE")) != NULL)
    {
        result = FileIncludeProcessor(task, attrib->value, htmlMarkup);
        DestroyAttribute(attrib);
        FreeMemory(attrib);
    }

    else if ((attrib = UnlinkAttributeInMarkup(htmlMarkup, "SEARCH")) != NULL)
    {
        /* set the include search path to what was specified */
        if(attrib->value != NULL)
        {
            StringCopy(searchPath, attrib->value, SEARCH_PATH_SIZE);
        }
        else
        {
            /* search path is cleared */
            searchPath[0] = NUL;
        }
        result = DISCARD_MARKUP;

        DestroyAttribute(attrib);
        FreeMemory(attrib);
    }
    else if ((attrib = UnlinkAttributeInMarkup(htmlMarkup, "NAME")) != NULL)
    {
        /* if a NAME attribute is found, and it contains a value, use
         * the ExternalFileProcessor to create the plaintext (this
         * function only reports output file's time, date, name) 
         */
        if (attrib->value != NULL)
        {
            result =  ExternalFileProcessor(task, htmlMarkup, attrib->value,
                                            newPlaintext);
        }
        else
        {
            /* <FILE NAME> should be repleaced by output file name */
            *newPlaintext = DuplicateString(task->outfile->name);
            HtpMsg(MSG_INFO, task->outfile, "adding output filename");
            result = NEW_MARKUP;
        }
        DestroyAttribute(attrib);
        FreeMemory(attrib);
    }
    else
    {
        /* Invoke ExternalFileProcessor on current input file, to
         * handle TIME, DATE attributes
         */
        result = ExternalFileProcessor(task, htmlMarkup, 
                                       task->sourceFilename, newPlaintext);
    }

    if (result != MARKUP_ERROR && htmlMarkup->attrib != NULL)
    {
        HtpMsg(MSG_WARNING, task->infile, 
               "Unhandled %s attribute in %s markup", 
               htmlMarkup->attrib->name, htmlMarkup->tag);
    }

    /* the new plaintext has been created */
    return result;
}   

uint OutputProcessor(TASK *task, HTML_MARKUP *htmlMarkup, char **newPlaintext)
{
    STREAM outputfile;
    HTML_ATTRIBUTE *attrib;
    BOOL append, expand, result;

    UNREF_PARAM(newPlaintext);

    /* get the filename to include */
    if ((attrib = UnlinkAttributeInMarkup(htmlMarkup, "FILE")) == NULL)
    {
        HtpMsg(MSG_ERROR, task->infile, "output filename not specified");
        return MARKUP_ERROR;
    }
    append = UnlinkBoolAttributeInMarkup(htmlMarkup, "APPEND");
    expand = UnlinkBoolAttributeInMarkup(htmlMarkup, "EXPAND");

    if (htmlMarkup->attrib != NULL) {
        HtpMsg(MSG_WARNING, task->infile, 
               "Unhandled %s attribute in %s markup", 
               htmlMarkup->attrib->name, htmlMarkup->tag);
    }

    /* open the output file */
    if(CreateFileWriter(&outputfile, attrib->value, append) == FALSE)
    {
        HtpMsg(MSG_ERROR, task->infile, 
               "unable to open output file \"%s\"", attrib->value);
        return MARKUP_ERROR;
    }
    DestroyAttribute(attrib);
    FreeMemory(attrib);

    result = ReadBlockToFile(task, expand, htmlMarkup->tag, &outputfile);
    FreeMemory((char *) outputfile.name);
    CloseStream(&outputfile);

    return result == FALSE ? MARKUP_ERROR : DISCARD_MARKUP;
}
