/* dircproxy
 * Copyright (C) 2002 Scott James Remnant <scott@netsplit.com>.
 * All Rights Reserved.
 *
 * irc_log.c
 *  - Handling of log files
 *  - Handling of log programs
 *  - Recalling from log files
 * --
 * @(#) $Id: irc_log.c,v 1.35.2.1 2002/09/09 12:24:07 scott Exp $
 *
 * This file is distributed according to the GNU General Public
 * License.  For full details, read the top of 'main.c' or the
 * file called COPYING that was distributed with this code.
 */

#include <pwd.h>
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdarg.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <time.h>

#include <dircproxy.h>
#include "net.h"
#include "sprintf.h"
#include "irc_net.h"
#include "irc_prot.h"
#include "irc_client.h"
#include "irc_string.h"
#include "irc_log.h"

/* forward declarations */
static void _irclog_close(struct logfile *);
static struct logfile *_irclog_getlog(struct ircproxy *, const char *);
static char *_irclog_read(FILE *);
static void _irclog_printf(FILE *, const char *, ...);
static int _irclog_write(struct logfile *, const char *, ...);
static int _irclog_pipe(struct logfile *log, const char *, const char *,
                        const char *);
static int _irclog_writetext(struct ircproxy *, struct logfile *, const char *,
                             const char *, const char *);
static int _irclog_text(struct ircproxy *, const char *, const char *,
                        const char *);
static int _irclog_recall(struct ircproxy *, struct logfile *, unsigned long,
                          unsigned long, const char *, const char *);

/* Log time format for strftime(3) */
#define LOG_TIME_FORMAT "%H:%M"

/* Log time/date format for strftime(3) */
#define LOG_TIMEDATE_FORMAT "%a, %d %b %Y %H:%M:%S %z"

/* Define MIN() */
#ifndef MIN
#define MIN(x, y) ((x) < (y) ? (x) : (y))
#endif /* MIN */

/* Create a temporary directory for log files */
int irclog_maketempdir(struct ircproxy *p) {
  struct passwd *pw;
  static unsigned int counter = 0;

  if (p->temp_logdir)
    return 0;

  pw = getpwuid(geteuid());
  if (pw) {
    struct stat statinfo;
    char *tmpdir;

    tmpdir = getenv("TMPDIR");
    if (!tmpdir)
      tmpdir = getenv("TEMP");
    debug("TMPDIR = '%s'", (tmpdir ? tmpdir : "(null)"));
    debug("Username = '%s'", pw->pw_name);
    debug("PID = '%d'", getpid());
    p->temp_logdir = x_sprintf("%s/%s-%s-%d-%d", (tmpdir ? tmpdir : "/tmp"),
                               PACKAGE, pw->pw_name, getpid(), counter++);
    debug("temp_logdir = '%s'", p->temp_logdir);

    if (lstat(p->temp_logdir, &statinfo)) {
      if (errno != ENOENT) {
        syscall_fail("lstat", p->temp_logdir, 0);
        free(p->temp_logdir);
        p->temp_logdir = 0;
        return -1;
       } else if (mkdir(p->temp_logdir, 0700)) {
        syscall_fail("mkdir", p->temp_logdir, 0);
        free(p->temp_logdir);
        p->temp_logdir = 0;
        return -1;
      }
    } else if (!S_ISDIR(statinfo.st_mode)) {
      debug("Existed, but not directory");
      free(p->temp_logdir);
      p->temp_logdir = 0;
      return -1;
    }
  } else {
    debug("Couldn't get username!");
    return -1;
  }

  return 0;
}

/* Clean up temporary directory of log files */
int irclog_closetempdir(struct ircproxy *p) {
  if (!p->temp_logdir)
    return 0;

  debug("Freeing '%s'", p->temp_logdir);
  rmdir(p->temp_logdir);
  free(p->temp_logdir);
  p->temp_logdir = 0;
  return 0;
}

/* Initialise a log file, opening the copy if necessary */
int irclog_init(struct ircproxy *p, const char *to) {
  char *ptr, *filename, *copydir, *copyfile;
  struct logfile *log;

  log = _irclog_getlog(p, to);
  if (!log)
    return -1;

  if (!p->temp_logdir)
    return -1;

  /* Store the config */
  if (log == &(p->other_log)) {
    ptr = filename = x_strdup("other");
    log->maxlines = p->conn_class->other_log_maxsize;
    log->always = p->conn_class->other_log_always;
    log->timestamp = p->conn_class->other_log_timestamp;
    log->relativetime = p->conn_class->other_log_relativetime;
    log->program = (p->conn_class->other_log_program
                    ? x_strdup(p->conn_class->other_log_program) : 0);
    copydir = (p->conn_class->other_log_copydir
               ? p->conn_class->other_log_copydir : 0);
  } else {
    ptr = filename = x_strdup(to);
    irc_strlwr(filename);
    log->maxlines = p->conn_class->chan_log_maxsize;
    log->always = p->conn_class->chan_log_always;
    log->timestamp = p->conn_class->chan_log_timestamp;
    log->relativetime = p->conn_class->chan_log_relativetime;
    log->program = (p->conn_class->chan_log_program
                    ? x_strdup(p->conn_class->chan_log_program) : 0);
    copydir = (p->conn_class->chan_log_copydir
               ? p->conn_class->chan_log_copydir : 0);
  }

  /* Channel names are allowed to contain . and / according to the IRC
     protocol.  These are nasty as it means someone could theoretically
     create a channel called #/../../etc/passwd and the program would try
     to unlink "/tmp/#/../../etc/passwd" = "/etc/passwd".  If running as root
     this could be bad.  So to compensate we replace '/' with ':' as thats not
     valid in channel names. */
  while (*ptr) {
    switch (*ptr) {
      case '/':
        *ptr = ':';
        break;
    }

    ptr++;
  }

  /* Store the filename */
  if (log->filename)
    free(log->filename);
  log->filename = x_sprintf("%s/%s", p->temp_logdir, filename);
  log->made = 0;
  debug("log->filename = '%s'", log->filename);

  /* Work out the copy filename, and clean up */
  copyfile = (copydir ? x_sprintf("%s/%s", copydir, filename) : 0);
  log->copy = 0;
  debug("copyfile = '%s'", (copyfile ? copyfile : ""));
  free(filename);

  /* Open and append to existing files (as long as they are files) */
  if (copyfile) {
    struct stat statinfo;

    if (lstat(copyfile, &statinfo)) {
      if (errno != ENOENT) {
        syscall_fail("lstat", copyfile, 0);
        free(copyfile);
        copyfile = 0;
      }
    } else if (!S_ISREG(statinfo.st_mode)) {
      debug("File existed, but wasn't a file");
      free(copyfile);
      copyfile = 0;
    }

    if (copyfile) {
      log->copy = fopen(copyfile, "a+");
      if (!log->copy)
        syscall_fail("fopen", copyfile, 0);
    }

    free(copyfile);
  }

  /* If we opened a copy file, write to it */
  if (log->copy) {
    char tbuf[40];
    time_t now;

    /* Output a "logging began" line */
    time(&now);
    strftime(tbuf, sizeof(tbuf), LOG_TIMEDATE_FORMAT, localtime(&now));
    _irclog_printf(log->copy, "* Logging started %s\n", tbuf);
  }

  return 0;
}

/* Free up log file information */
void irclog_free(struct logfile *log) {
  if (!log->filename)
    return;

  if (log->open)
    _irclog_close(log);

  debug("Freeing up log file '%s'", log->filename);

  if (log->copy) {
    /* Output a "logging ended" line to the copy */
    char tbuf[40];
    time_t now;

    time(&now);
    strftime(tbuf, sizeof(tbuf), LOG_TIMEDATE_FORMAT, localtime(&now));
    _irclog_printf(log->copy, "* Logging finished %s\n", tbuf);

    fclose(log->copy);
    log->copy = 0;
  }

  unlink(log->filename);
  free(log->filename);
  free(log->program);
  log->nlines = 0;
  log->made = 0;
}

/* Open a previously init'd log file. 0 = ok, -1 = error */
int irclog_open(struct ircproxy *p, const char *to) {
  struct logfile *log;

  log = _irclog_getlog(p, to);
  if (!log || !log->filename)
    return -1;
  if (log->open)
    return 0;

  /* Unlink first for security, then open w+ */
  if (unlink(log->filename) && (errno != ENOENT)) {
    syscall_fail("unlink", log->filename, 0);
    free(log->filename);
    log->filename = 0;
    return -1;
  }

  log->file = fopen(log->filename, "w+");
  if (!log->file) {
    syscall_fail("fopen", log->filename, 0);
    free(log->filename);
    log->filename = 0;
    return -1;
  }
  log->open = log->made = 1;
  
  if (chmod(log->filename, 0600))
    syscall_fail("chmod", log->filename, 0);
  
  log->nlines = 0;

  return 0;
}

/* Close a log file */
void irclog_close(struct ircproxy *p, const char *to) {
  struct logfile *log;
  
  log = _irclog_getlog(p, to);
  if (!log)
    return;

  _irclog_close(log);
}

/* Actually close a log file */
static void _irclog_close(struct logfile *log) {
  if (!log->open)
    return;

  debug("Closing log file '%s'", log->filename);
  fclose(log->file);
  log->open = 0;
}

/* Get a log file structure out of an ircproxy */
static struct logfile *_irclog_getlog(struct ircproxy *p, const char *to) {
  struct ircchannel *c;
  
  if (!to)
    return 0;

  c = ircnet_fetchchannel(p, to);
  if (c) {
    return &(c->log);
  } else {
    return &(p->other_log);
  }
}

/* Read a line from the log */
static char *_irclog_read(FILE *file) {
  char buff[512], *line;

  line = 0;
  while (1) {
    if (!fgets(buff, 512, file)) {
      free(line);
      return 0;
    } else if (!strlen(buff)) {
      free(line);
      return 0;
    } else {
      char *ptr;

      if (line) {
        char *new;

        new = x_sprintf("%s%s", line, buff);
        free(line);
        line = new;
      } else {
        line = x_strdup(buff);
      }

      ptr = line + strlen(line) - 1;
      if (*ptr == '\n') {
        while ((ptr >= line) && (!ptr || strchr(" \t\r\n", *ptr))) *(ptr--) = 0;
        break;
      }
    }
  }

  return line;
}

/* Write a line to the end of a file */
static void _irclog_printf(FILE *fd, const char *format, ...) {
  va_list ap;
  char *msg;

  va_start(ap, format);
  msg = x_vsprintf(format, ap);
  va_end(ap);

  fseek(fd, 0, SEEK_END);
  fputs(msg, fd);
  fflush(fd);

  free(msg);
}

/* Write a line to the log */
static int _irclog_write(struct logfile *log, const char *format, ...) {
  va_list ap;
  char *msg;

  va_start(ap, format);
  msg = x_vsprintf(format, ap);
  va_end(ap);

  if (log->open && log->maxlines && (log->nlines >= log->maxlines)) {
    FILE *out;
    char *l;

    /* We can't simply add .tmp or something on the end, because there is
       always a possibility that might be a channel name.  Besides using
       temporary files always looks icky to me.  This "Sick Puppy" way of
       reading from an unlinked file sits with me much better (says a lot
       about me, that) */
    fseek(log->file, 0, SEEK_SET);
    unlink(log->filename);

    /* This *really* shouldn't happen */
    out = fopen(log->filename, "w+");
    if (!out) {
      syscall_fail("fopen", log->filename, 0);
      return -1;
    }

    /* Make sure it's got the right permissions */
    if (chmod(log->filename, 0600))
      syscall_fail("chmod", log->filename, 0);

    /* Eat from the start */
    while ((log->nlines >= log->maxlines) && (l = _irclog_read(log->file))) {
      free(l);
      log->nlines--;
    }

    /* Write the rest */
    while ((l = _irclog_read(log->file))) {
      fprintf(out, "%s\n", l);
      free(l);
    }

    /* Close the input file, thereby *whoosh*ing it */
    fclose(log->file);
    log->file = out;
  }

  /* Write to the log file */
  if (log->open) {
    _irclog_printf(log->file, "%s\n", msg);
    log->nlines++;
  }

  /* Write to the copy too */
  if (log->copy)
    _irclog_printf(log->copy, "%s\n", msg);

  free(msg);
  return 0;
}

/* Write a line through a pipe to a given program */
static int _irclog_pipe(struct logfile *log, const char *to, const char *from,
                        const char *text) {
  int p[2], pid;

  if (!log->program)
    return 1;

  /* Prepare a pipe */
  if (pipe(p)) {
    syscall_fail("pipe", 0, 0);
    return 1;
  }

  /* Do the fork() thing */
  pid = fork();
  if (pid == -1) {
    syscall_fail("fork", 0, 0);
    return 1;

  } else if (pid) {
    FILE *fd;

    /* Parent - write text to a file descriptor of the pipe */
    close(p[0]);
    fd = fdopen(p[1], "w");
    if (!fd) {
      syscall_fail("fdopen", 0, 0);
      close(p[1]);
      return 1;
    }
    fprintf(fd, "%s\n", text);
    fflush(fd);
    fclose(fd);

  } else {
    /* Child, copy pipe to STDIN then exec the process */
    close(p[1]);
    if (dup2(p[0], STDIN_FILENO) != STDIN_FILENO) {
      syscall_fail("dup2", 0, 0);
      close(p[0]);
      return 1;
    }
   
    execlp(log->program, log->program, from, (to ? to : ""), 0);

    /* We can't get here.  Well we can, it means something went wrong */
    syscall_fail("execlp", log->program, 0);
    exit(10);
  }

  return 0;
}

/* Write a PRIVMSG to log file(s) */
int irclog_msg(struct ircproxy *p, const char *to, const char *nick,
               const char *format, ...) {
  char *from, *text;
  va_list ap;
  int ret;

  va_start(ap, format);
  from = x_sprintf("<%s>", nick);
  text = x_vsprintf(format, ap);
  ret = _irclog_text(p, to, from, text);
  free(text);
  free(from);
  va_end(ap);

  return ret;
}

/* Write a NOTICE to log file(s) */
int irclog_notice(struct ircproxy *p, const char *to, const char *nick,
                  const char *format, ...) {
  char *from, *text;
  va_list ap;
  int ret;

  va_start(ap, format);
  from = x_sprintf("-%s-", nick);
  text = x_vsprintf(format, ap);
  ret = _irclog_text(p, to, from, text);
  free(text);
  free(from);
  va_end(ap);

  return ret;
}

/* Write a CTCP to log file(s) */
int irclog_ctcp(struct ircproxy *p, const char *to, const char *nick,
                const char *format, ...) {
  char *from, *text;
  va_list ap;
  int ret;

  va_start(ap, format);
  from = x_sprintf("[%s]", nick);
  text = x_vsprintf(format, ap);
  ret = _irclog_text(p, to, from, text);
  free(text);
  free(from);
  va_end(ap);

  return ret;
}

/* Write some text to a log file */
static int _irclog_writetext(struct ircproxy *p, struct logfile *log,
                             const char *to, const char *from,
                             const char *text) {
  if (log->timestamp) {
    time_t now;

    time(&now);
    if (p->conn_class->log_timeoffset)
      now -= (p->conn_class->log_timeoffset * 60);

    if (log->relativetime) {
      _irclog_write(log, "@%lu %s %s", now, from, text);
    } else {
      char tbuf[40];

      strftime(tbuf, sizeof(tbuf), LOG_TIME_FORMAT, localtime(&now));
      _irclog_write(log, "%s [%s] %s", from, tbuf, text);
    }
  } else {
    _irclog_write(log, "%s %s", from, text);
  }

  _irclog_pipe(log, to, from, text);

  return 0;
}

/* Write some text to log file(s) */
static int _irclog_text(struct ircproxy *p, const char *to, const char *from,
                        const char *text) {
  if (to) {
    struct logfile *log;
    
    /* Write to one file */
    log = _irclog_getlog(p, to);
    if (!log)
      return -1;

    _irclog_writetext(p, log, to, from, text);
  } else {
    struct ircchannel *c;

    /* Write to all files */
    _irclog_writetext(p, &(p->other_log), (p->nickname ? p->nickname : ""),
                      from, text);
    c = p->channels;
    while (c) {
      _irclog_writetext(p, &(c->log), c->name, from, text);
      c = c->next;
    }
  }

  return 0;
}

/* Called to automatically recall stuff */
int irclog_autorecall(struct ircproxy *p, const char *to) {
  unsigned long recall, start, lines;
  struct logfile *log;

  log = _irclog_getlog(p, to);
  if (!log)
    return -1;

  if (log == &(p->other_log)) {
    recall = p->conn_class->other_log_recall;
  } else {
    recall = p->conn_class->chan_log_recall;
  }

  /* Don't recall anything */
  if (!recall)
    return 0;
  
  /* Recall everything */
  if (recall == -1) {
    start = 0;
  } else {
    start = (recall > log->nlines ? 0 : log->nlines - recall);
  }
  lines = log->nlines - start;

  return _irclog_recall(p, log, start, lines, to, 0);
}

/* Called to manually recall stuff */
int irclog_recall(struct ircproxy *p, const char *to,
                  long start, long lines, const char *from) {
  struct logfile *log;

  log = _irclog_getlog(p, to);
  if (!log)
    return -1;

  /* Recall everything */
  if (lines == -1) {
    start = 0;
    lines = log->nlines;
  }

  /* Recall recent */
  if (start == -1)
    start = (lines > log->nlines ? 0 : log->nlines - lines);

  return _irclog_recall(p, log, start, lines, to, from);
}

/* Called to do the recall from a log file */
static int _irclog_recall(struct ircproxy *p, struct logfile *log,
                          unsigned long start, unsigned long lines,
                          const char *to, const char *from) {
  FILE *file;
  int close;

  /* If the file isn't open, we have to open it and remember to close it
     later */
  if (log->open) {
    file = log->file;
    close = 0;
  } else if (log->filename && log->made) {
    file = fopen(log->filename, "r");
    if (!file) {
      ircclient_send_notice(p, "Couldn't open log file %s", log->filename);
      syscall_fail("fopen", log->filename, 0);
      return -1;
    }
    close = 1;
  } else {
    return -1;
  }

  /* Jump to the beginning */
  fseek(file, 0, SEEK_SET);

  if (start < log->nlines) {
    char *l;

    /* Skip start lines */
    while (start && (l = _irclog_read(file))) {
      free(l);
      start--;
    }

    /* Make lines sensible */
    lines = MIN(lines, log->nlines - start);

    /* Recall lines */
    while (lines && (l = _irclog_read(file))) {
      time_t when = 0;
      char *ll;

      /* Timestamped line for relative log creation */
      ll = l;
      if (*l == '@') {
        char *ts, *rest;

        /* Timestamp one character in */
        ts = l + 1;
        if (!*ts) {
          free(ll);
          continue;
        }

        /* Message continues after a space */
        rest = strchr(l, ' ');
        if (!rest) {
          free(ll);
          continue;
        }

        /* Delete the space */
        *(rest++) = 0;
        l = rest;

        /* Obtain the timestamp */
        when = strtoul(ts, (char **)NULL, 10);
      }

      /* Message or Notice lines, these require a bit of parsing */
      if ((*l == '<') || (*l == '-') || (*l == '[')) {
        char *src, *msg, lastchar;

        /* Source starts one character in */
        src = l + 1;
        if (!*src) {
          free(ll);
          continue;
        }

        /* Message starts after a space and the correct closing character */
        msg = strchr(l, ' ');
        if (*l == '<') {
          lastchar = '>';
        } else if (*l == '[') {
          lastchar = ']';
        } else {
          lastchar = '-';
        }
        if (!msg || (*(msg - 1) != lastchar)) {
          free(ll);
          continue;
        }

        /* Delete the closing character and skip the space */
        *(msg - 1) = 0;
        msg++;

        /* Do filtering */
        if (from) {
          char *comp, *ptr;

          /* We just check the nickname, so strip off anything after the ! */
          comp = x_strdup(src);
          if ((ptr = strchr(comp, '!')))
            *ptr = 0;

          /* Check the nicknames are the same */
          if (irc_strcasecmp(comp, from)) {
            free(comp);
            free(ll);
            continue;
          }
          free(comp);
        }

        /* If there was a timestamp on it, we either fake the old-style
           stuff or do the new fancy stuff */
        if (when && log->timestamp) {
          char tbuf[40];


          if (log->relativetime) {
            time_t now, diff;

            time(&now);
            diff = now - when;

            if (diff < 82800L) {
              /* Within 23 hours [hh:mm] */
              strftime(tbuf, sizeof(tbuf), "%H:%M", localtime(&when));
            } else if (diff < 518400L) {
              /* Within 6 days [day hh:mm] */
              strftime(tbuf, sizeof(tbuf), "%a %H:%M", localtime(&when));
            } else if (diff < 25920000L) {
              /* Within 300 days [d mon] */
              strftime(tbuf, sizeof(tbuf), "%d %b", localtime(&when));
            } else {
              /* Otherwise [d mon yyyy] */
              strftime(tbuf, sizeof(tbuf), "%d %b %Y", localtime(&when));
            }
          } else {
            strftime(tbuf, sizeof(tbuf), LOG_TIME_FORMAT, localtime(&when));
          }

          /* Send the line */
          if (*l == '[') {
            char *cmd;

            /* Its a CTCP, so we have to place the command before the
               timestamp */
            cmd = msg;
            msg += strcspn(msg, " ");
            if (*msg)
              *(msg++) = 0;

            net_send(p->client_sock, ":%s PRIVMSG %s :\001%s [%s]%s%s\001\r\n",
                     src, to, cmd, tbuf, (strlen(msg) ? " " : ""), msg);
          } else {
            net_send(p->client_sock, ":%s %s %s :[%s] %s\r\n", src,
                     (*l == '<' ? "PRIVMSG" : "NOTICE"), to, tbuf, msg);
          }
        } else {
          /* Send the line */
          if (*l == '[') {
            net_send(p->client_sock, ":%s PRIVMSG %s :\001%s\001\r\n", src,
                     to, msg);
          } else {
            net_send(p->client_sock, ":%s %s %s :%s\r\n", src,
                     (*l == '<' ? "PRIVMSG" : "NOTICE"), to, msg);
          }
        }

      } else if (strncmp(l, "* ", 2)) {
        /* Anything thats not a comment gets sent as a notice */
        ircclient_send_notice(p, "%s", l);

      }

      free(ll);
      lines--;
    }
  }

  /* Either close, or skip back to the end */
  if (close) {
    fclose(file);
  } else {
    fseek(file, 0, SEEK_END);
  }
  return 0;
}


syntax highlighted by Code2HTML, v. 0.9.1