/*
 * logfile.cpp
 *
 * (c) 1999-2002 Murat Deligonul
 * 
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.  
 *
 */
 

#include "autoconf.h"
#include <sys/types.h>
#include <unistd.h>
#include <sys/stat.h>
#include <limits.h>
#include <stdlib.h>
#include <ctype.h>
#include <dirent.h>
#include "debug.h"
#include "linkedlist.h"                     
#include "ezbounce.h"                            
#include "general.h"
#include "logfile.h"
#include "server.h"

static char * cheesy_encrypt(char *);
static char * cheesy_decrypt(char *);

list<char> logfile::active_logs;

/*
 * open()'s a new logfile, ensuring that is unique and not locked by anyone
 * else
 */
/* static */ int logfile::mkname(const char * basedir, const char * owner, int id, 
                                  const char * pw, const char * type, char * buff)
{
    static const char * const extra[] = { "", "1.", "2.", "3.", "4.", "5.", "6." };
    int c = 0, fd = -1;
    while (1)
    {
        sprintf(buff, "%s/%d %s %d %s %s.%slog", basedir, getpid(), owner, id, pw, type, extra[c]); 
        if (is_locked(buff)
           || (fd = open(buff, O_CREAT | O_APPEND | O_WRONLY, 0600)) < 0)
        {
            if (fd > -1)
            {
                close(fd);
                fd = -1;
            }
            if (++c > 6)
                return -1;
            continue;
        } else if (fd > -1)
            break;
    }
    return fd;
}
/*
 * Updates result on leave. Can be:
 *  1 = Ok
 *  0 = Errors, but can go on
 * -1 = Fatal
 */
logfile::logfile(const char * basedir, const char * owner, int id, int options, int maxsize, const char * password, int * result)
{
    char filebuff[PATH_MAX + 1];
    char * pw = (password ?  my_strdup(password) : my_strdup("none"));
    
    pw = cheesy_encrypt(pw);
    
    log1 = log2 = NULL;
    fd1 = fd2 = -1;
    this->options = options;
    this->maxsize = maxsize;
    
    /* Create the log files now */
    if (options & LOG_SEPERATE)
    {
        if (options & LOG_CHANNEL)
        {
            /* Assemble the file name */
            fd1 = mkname(basedir, owner, id, pw, "(channel)", filebuff);
            if (fd1 < 0)
                *result = 0;
            else
                log1 = my_strdup(filebuff);
        }
        if (options & LOG_PRIVATE)
        {
            /* Assemble the file name */
            fd2 = mkname(basedir, owner, id, pw, "(private)", filebuff);
            if (fd2 < 0)
                *result = 0;
            else
                log2 = my_strdup(filebuff);
        }
        
        if (fd1 < 0 && fd2 >= 0)
            fd1 = fd2;
        else if (fd2 < 0 && fd1 >= 0)
            fd2 = fd1;
    } else {
        if ((options & LOG_CHANNEL) || (options & LOG_PRIVATE))
            fd1 = mkname(basedir, owner, id, pw, "(all)", filebuff);
        if (fd1 < 0)
        {
            *result = -1;
            delete[] pw;
            return;
        } 
        log1 = my_strdup(filebuff);
    }

    delete[] pw;            

    if (fd2 < 0 && fd1 < 0)
    {
        *result = -1;
        return;
    }
    
    struct stat st;

    fstat(fd1, &st);
    size1 = st.st_size;
    fstat(fd2, &st);
    size2 = st.st_size;

    if ((maxsize) && (size1 > (unsigned) maxsize || size2 > (unsigned) maxsize))
    {
        *result = -1;
        DEBUG("Log file already too big; aborting.\n");
        close(fd1);
        if (fd2 >= 0 && fd2 != fd1) 
            close(fd2); 
        fd1 = fd2 = -1;
        return;
    }

    /* And I think we're ready after this */
    if (log1)
        lock(log1);
    if (fd1 != fd2 && log2) 
        lock(log2);
    
    fdprintf(fd1, "******************************************************\n"
                  "ezbounce log file started at %s\n"
                  "******************************************************\n", 
                   timestamp());  
    if (fd2 != fd1)
        fdprintf(fd2, "******************************************************\n"
                      "ezbounce log file started at %s\n"
                      "******************************************************\n", 
                   timestamp());  
    *result = 1;
}                            


void logfile::stop(void)
{
    if (fd1 > -1)
    {
        fdprintf(fd1, "******************************************************\n"
                      "ezbounce log file stopped at %s\n"
                      "******************************************************\n", 
                     timestamp());
        close(fd1);
    }
    if (fd2 != fd1)
    {
        fdprintf(fd2, "******************************************************\n"
                      "ezbounce log file stopped at %s\n"
                      "******************************************************\n", 
                     timestamp());
        close(fd2);
    }
    fd1 = fd2 = -1;
}

logfile::~logfile(void)
{
    stop();
    if (log1)
        release(log1);
    if (log2)
        release(log2);
    delete[] log1;
    delete[] log2;
}


/*
 * The interface to the log files -- we do the parsing
 * elsewhere.. Arguments:
 *
 *  flags           - combination of options to specify the event
 * source           - who it's coming from (in a full ircaddr structure)
 * target           - who it's to
 * text             - chat text, or part & quit part messages
 *                    this is not const; we need to be able to modify it
 *                    so we can get rid of the color codes and stuff
 * extra            - for anything that don't fit; for example in KICK, who got kicked
 *                    or in CTCPs, the CTCP itself, like CLIENTINFO or VERSION or whatever
 */

int logfile::write(int flags, const __ircaddr * source, const char * target, char * text, const char * extra)
{
    int target_fd = fd1;
    unsigned long * size = &size1;
    int written = 0;

    if (flags & EVENT_PRIVATE)
    {
        if (!(options & LOG_PRIVATE))
            return 0;
        if (!log2)
        {
            size = &size1;
            target_fd = fd1;
        } else {
            size = &size2;
            target_fd = fd2;
        }
    }

    /* Enforce the size limits now */
    if ((maxsize) && (*size > (unsigned) maxsize))
    {
        fdprintf(target_fd, "*** [NOTE] Logfile size limit of %d bytes exceeded; stopping log ***\n", maxsize);
        fdprintf(target_fd, "******************************************************\n"
                        "ezbounce log file stopped at %s\n"
                        "******************************************************\n", timestamp());
        close(target_fd);
        if (target_fd == fd1)
            fd1 = -1;
        else if (target_fd == fd2)
            fd2 = -1;
        return 0;
    }
    else if (flags & EVENT_PUBLIC)
    {
        /* Check if we really need to log */
        if (!(options & LOG_CHANNEL))
            return 0;
    }

    if (text)
        strip_burc_codes(text);

    if (options & LOG_TIMESTAMP)
        written += fdprintf(target_fd, "%s ", timestamp(1));
    if (flags & EVENT_NOTICE)
        written += fdprintf(target_fd, "-%s!%s@%s:%s- %s\n", source->nick,
                                    source->ident, source->host, target, text);
    else if (flags & EVENT_JOIN)
        written += fdprintf(target_fd, "*** %s (%s@%s) has JOINed %s\n", source->nick,
                                    source->ident, source->host, target);
    else if (flags & EVENT_PART)
        written += fdprintf(target_fd, "*** %s (%s@%s) has PARTed %s (%s)\n", source->nick,
                                    source->ident, source->host, target, text);
    else if (flags & EVENT_TOPIC)
        written += fdprintf(target_fd, "*** %s changes %s TOPIC to: %s\n", source->nick, target, text);
    else if (flags & EVENT_KICK)
        written += fdprintf(target_fd, "*** %s (%s@%s) has KICKed %s from %s (%s)\n", source->nick, source->ident,
                                    source->host, extra, target, text);
    else if (flags & EVENT_MODE)
        written += fdprintf(target_fd, "*** %s changes %s MODE to: %s\n", source->nick, target, text);
    else if (flags & EVENT_QUIT)
        written += fdprintf(target_fd, "*** %s (%s@%s) has QUIT (%s)\n", source->nick, source->ident,
                                                                    source->host, text);
    else if (flags & EVENT_NICK)
        written += fdprintf(target_fd, "*** %s changes NICK to %s\n", source->nick, target);
    else if (flags & EVENT_CTCP)
    {
        /* Remove ending 001 char */
        if (text)
        {
            char * dummy = &text[strlen(text)] - 1;
            if (*dummy == '\001')
                *dummy = 0;
        }

        if (flags & EVENT_PUBLIC)
            written += fdprintf(target_fd, "[%s] ", target);

        if (strcmp(extra, "ACTION") == 0)
        {
            if ((options & LOG_FULL_ADDRS) || (flags & EVENT_PRIVATE))
                written += fdprintf(target_fd, "* %s!%s@%s %s\n", source->nick, source->ident, source->host, extra);
            else
                written += fdprintf(target_fd, "* %s %s\n", source->nick, extra);
        }
        else
            written += fdprintf(target_fd, "*** CTCP %s [%s] from %s (%s@%s) to %s\n", extra, text, source->nick,
                                        source->ident, source->host, target);
    } else {
        if (flags & EVENT_PUBLIC)
            written += fdprintf(target_fd, "[%s] ", target);

        if ((options & LOG_FULL_ADDRS) || (flags & EVENT_PRIVATE))
           written += fdprintf(target_fd, "<%s!%s@%s> %s\n", source->nick, source->ident, source->host, text);
        else
           written += fdprintf(target_fd, "<%s> %s\n", source->nick, text);
    }
    if (written >= 0)
        *size += (unsigned long) written;
    return written;
}

/*
 * Convert character log control codes to logfile class
 * flags. Valid codes:
 * f -  full user address in public channel messages
 * p -  private
 * c -  channel
 * a -  both channel and private
 * n -  none (causes immediate return 
 * s -  log seperately
 * t -  timestamp all events
 */
/* static */ int logfile::charflags_to_int(const char * stuff)
{
    int flags = 0;
    for (unsigned x = 0; x < strlen(stuff); x++)
    {
        switch (tolower(stuff[x]))
        {
        case 'c':
            flags |= LOG_CHANNEL;
            break;
        case 'p':
            flags |= LOG_PRIVATE;
            break;
        case 'a':
            flags |= LOG_ALL;
            break;
        case 'n':            
            return LOG_NONE;
        case 's':
            flags |= LOG_SEPERATE;
            break;
        case 't':
            flags |= LOG_TIMESTAMP;
            break;
        case 'f':
            flags |= LOG_FULL_ADDRS;
            break;
        }
    }
    return flags;
}

/* 
 * Not going to bother with checks.
 * Buffer had better be able to hold 15 chars.  
 * Return size, not including the null char */
/* static */ int logfile::intflags_to_char(int flags, char * buff)
{
    int p = 0;
    if (flags & LOG_SEPERATE)
        buff[p++] = 's';
    if ((flags & LOG_ALL) == LOG_ALL)
        buff[p++] = 'a';
    if (flags & LOG_PRIVATE)
        buff[p++] = 'p';
    if (flags & LOG_CHANNEL)
        buff[p++] = 'c';
    if (flags & LOG_FULL_ADDRS)
        buff[p++] = 'f';
    if (flags & LOG_TIMESTAMP)
        buff[p++] = 't';
    if (flags == LOG_NONE)
    {
        p = 0;
        buff[p++] = 'n';
    }
    buff[p] = 0;
    
    return p;
}

/* Not going to bother checking for exceeding size limit. */ 
int logfile::dump(const char * raw)
{
    int ret = -1;
    if (fd1 > -1)
    {
        ret = fdprintf(fd1, "%s", raw);
        size1 += ret;
    }
        
    if (fd2 != fd1)
        size2 += fdprintf(fd2, "%s", raw);

    return ret;
}


/* 
 * Fill an array with our file names.
 * We will always fill both elements.
 * f[0] will be all OR channel logs
 * f[1] will be NULL OR private logs
 */
int logfile::get_filenames(char *f[2])
{
    f[0] = my_strdup(log1);
    f[1] = my_strdup(log2);
    return 1;
}

/*
 * Remove Bold, Underline, Reverse, and Color codes from
 * the text. 
 */
int logfile::strip_burc_codes(char * text)
{
    static const char BOLD  = 2,
                      COLOR = 3,
                      ULINE = 21,
                      REVERSE = 18;
    
    /* Speed hack.. */

    if (!strchr(text,BOLD) && !strchr(text, COLOR) 
        && !strchr(text,ULINE) && !strchr(text, REVERSE))
        return 1;

    char * n = new char[strlen(text) + 1];    
    char * orig = text, *origN = n;

    while (*text)
    {
        if (*text == BOLD || *text == COLOR 
            || *text == ULINE || *text == REVERSE)
        {
            text++;
            continue;
        }
        if (*text == COLOR)
        {
            /* From what i can tell.. there seems to be no clear
             * standard as to how many digits the number after the
             * ^C will be, and how many digits the number after the
             * comma will be.. and I think the valid range of any
             * color code number is 0-15.
             */ 
            text++;

            if (isdigit(*text))
                text++;
            if (*text <= '5' && *text >= '0' && *(text - 1) == '1')
                text++;

            if (*text == ',')
            {
                text++;
                if (isdigit(*text))
                    text++;
                if (*text <= '5' && *text >= '0' && *(text - 1) == '1')
                    text++;            
            }

            continue;
        }

        *n++ = *text++;
    }
    *n = 0;
    strcpy(orig, origN);
    delete[] origN;

    /* there's gotta be a better way.. */
    return 1;
}
    
/* i don't know anything about encryption */
static char * cheesy_encrypt(char * text)
{
    unsigned int i;
    char c;
    for (i = 0; i < strlen(text); i++)
	{
        c = text[i];
        if (!(i % 2))
            c = (9 ^ c);
        else
            c = c ^ (i % c);    
        text[i] = c;
	}
	return text;
}

static char * cheesy_decrypt(char * text)
{
	unsigned int i;
    char c;
	for (i = 0; i < strlen(text); i++)
	{
            c = text[i];
            if (!(i % 2))
                c = (9 ^ c);
            else
                c = c ^ (i % c);
            text[i] = c;            
	}
	return text;         
}


/*
 * Finds log files matching requested properties in basedir.
 * First checks for ones under this pid then everything else is fair game.
 * Makes sure to not match log files that are currently being written
 * to. 
 *
 * Store up to two results in logfiles. Return number of results found.
 */

struct logfile_ent {
    int uid;
    pid_t pid;
    char * filename;
    char file_nick[NICKNAME_LENGTH];
    char file_pw[PASSWORD_LENGTH];
};


/*
 * Arguments: 
 *      basedir - where to find logs
 *      nickname, password, id, self explanatatory
 */
/* static */ int logfile::find_log_files(const char * basedir, const char * username,
                                         const char * password, int id, list<char> * plist, int max)
{
    DIR *dir = opendir(basedir);
    struct dirent *d = 0;
    int num_found = 0;
    static pid_t mypid = getpid();

    assert(username);
    if (!password)
        password = "none";

    if (!dir)
        return -1;
    
    list<struct logfile_ent> list;
    struct logfile_ent * lt = 0;

    errno = 0;

    while ((d = readdir(dir)))
    {
        int args;

        if (is_locked(d->d_name))
        {
            DEBUG("Log file %s found to be locked, skipping.\n", d->d_name);
            continue;
        }
        
        lt = (lt ? lt : new struct logfile_ent);
        
        /* First of all, check that the name matches the known pattern we use */
        args = sscanf(d->d_name, "%d %" STR_NICKNAME_LENGTH "s %d %" STR_PASSWORD_LENGTH "s", &lt->pid, lt->file_nick, &lt->uid, lt->file_pw);

        if (args < 4) 
            continue;

        /*
         * Check that the username matches
         *       that the passwords match (specify "none" if no pass!!)
         *       and that the id's match, if a valid id was provided
         */
        if (!strcasecmp(username, lt->file_nick)
             && !strcasecmp(password, cheesy_decrypt(lt->file_pw))
             && (id < 0 || id == lt->uid))
        {
            num_found++;
            lt->filename = new char[strlen(basedir) + strlen(d->d_name) + 5];
            sprintf(lt->filename, "%s/%s", basedir, my_strdup(d->d_name));
            list.add(lt);
            lt = 0; 
        }
    }
    
    closedir(dir);

    if (lt)
    {
        delete lt;
        lt = 0;
    }
    if (num_found)
    {
        int cur = 0;
        /* Go through the whole list, favor ones with current PID */
        for (int i = 0; i < num_found && cur < max; i++)
        {
            lt = list.get(i);
            if (lt->pid == mypid)
            {
                cur++;
                plist->add(my_strdup(lt->filename));
            }
        }

        /* Fill in other matches.    */
        for (int i = 0; i < num_found && cur < max; i++)
        {
            lt = list.get(i);
            if (lt->pid != mypid)
            {
                cur++;
                plist->add(my_strdup(lt->filename));
            }
        }

        lt = 0;
        
        /* We got the filenames now, so clean up the mess */
        destroy_list(&list, 1);
        return num_found;
    }    
    return 0;
}


/* static */ int logfile::lock(const char * file)
{
    DEBUG("logfile::lock() called on %s\n", file);
    file = nopath(file);
    if (!file || is_locked(file))
    {
        DEBUG("logfile::lock() on alreday locked file %s !!\n", file);
        return 0;
    }
    active_logs.add(my_strdup(file));
    return 1;
}

/* static */ int logfile::release(const char * file)
{
    file = nopath(file);
    DEBUG("logfile::release() called on %s\n", file);
    return strlist_remove(&active_logs, file);
}


/* static */ int logfile::is_locked(const char * file)
{
    list_iterator<char> i (&active_logs);
    file = nopath(file);
    DEBUG("Lock Check on file %s\n", file);
    while (i.has_next())
        if (strcmp(file, i.next()) == 0)
            return 1;

    return 0;
}


const char * logfile::fixup_logname(const char * filename)
{
    static char * fixup_buff;
    if (fixup_buff)
    {
        delete[] fixup_buff;
        fixup_buff = 0;
    }
    
    filename = nopath(filename);
    int len = strlen(filename) + 20;
    fixup_buff = new char[len];
    
    char nick[NICKNAME_LENGTH], id[10], suffix[20];
    time_t blah = ircproxy_time();
    struct tm * t = localtime(&blah);

    gettok(filename, nick, sizeof(nick), ' ', 2);
    gettok(filename, id, sizeof(id), ' ', 3);
    gettok(filename, suffix, sizeof(suffix), ' ', 5);

    snprintf(fixup_buff, len, "%d%02d%02d-%s-[%s]-%s", 1900 + t->tm_year, t->tm_mon + 1, t->tm_mday, nick, id, suffix);
    return fixup_buff;
}


syntax highlighted by Code2HTML, v. 0.9.1