/*
 * theme() applies a theme to a webpage
 *
 * usage: http://server/theme/path/to/page
 *
 * Theme takes the first element of the path, looks for a page.theme
 * file there, then concatenates that with the page (which is not to
 * be a real html file, just the content.)
 *
 * page.theme looks like
 *
 *   html html html html <?theme?> html html html
 */
#include "config.h"

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <pwd.h>
#include <errno.h>
#include <stdlib.h>
#include <stdarg.h>
#include <string.h>
#include <syslog.h>
#include <magic.h>

#if HAVE_MMAP
# include <sys/mman.h>
#endif

static char *path = 0;
static char marker[] = "<?theme ";
static char themable[] = PAGES;

static int avec_headers = 1;		/* needs ``Content-Type:'' as cgi */
static char *meta = 0;
static int szmeta = 0;
static char *rc = 0;			/* path to the theme file */
static char *document = 0;		/* path to the document */
static char *url = 0;			/* the whole path, including username */
static char *username = 0;		/* the user */
static FILE *page = 0;
static int doc = 0;

typedef struct variable {
    char *var;
    char *value;
    struct variable *next;
} Variable;

static Variable *variables = 0;

static magic_t poof;

void action(FILE*);

/*
 * spit out a server error page, then go thud
 */
void
falloverdead(int code, char *fmt, ...)
{
    va_list ap;

    if (avec_headers)
	printf("Content-Type: text/html\r\n\r\n");

    printf("<!DOCTYPE HTML PUBLIC \"-//IETF//DTD HTML 2.0//EN\">\n");
    printf("<HTML><HEAD><TITLE>%03d %s</TITLE></HEAD>\n",
	    code,
	    (errno==ENOENT) ? "Not Found" : "Ooops");
    printf("<BODY><H1>");
    va_start(ap, fmt);
    vprintf(fmt, ap);
    va_end(ap);
    printf("</H1><HR>\n");
    printf("<P ALIGN=RIGHT>Theme 1.17</P>\n");
    printf("</BODY></HTML>\n");
    exit(1);
}


/*
 * save a variable
 */
void
stash(char *var, char *value)
{
    Variable *tmp = malloc(sizeof *tmp);

    if (tmp) {
	tmp->var   = strdup(var);
	tmp->value = strdup(value);
	tmp->next = variables;
	variables = tmp;
    }
}


/*
 * return a pointer to a variable
 */
char *
fetch(char *name)
{
    Variable *p = variables;

    while (p)
	if (strcasecmp(p->var, name) == 0)
	    return p->value;
	else
	    p = p->next;
    return 0;
}


/*
 * <?theme $var?>
 */
void
putvariable(char *variable)
{
    char *p = fetch(variable);

    if (p) fwrite(p, 1, strlen(p), stdout);
}


/*
 * <?theme title?>
 */
void
putpagetitle()
{
    char *title = fetch("title");

    if (title)
	printf("<title>%s</title>", title);
}


/*
 * <?theme meta?>
 */
void
putpagemeta()
{
    if (meta) fwrite(meta, 1, strlen(meta), stdout);
}


/*
 * <?theme rc?>
 */
void
putrc()
{
    char *eop = strrchr(rc, '/');

    printf("/~%s/", username);
    if (eop)
	fwrite(rc, (int)(eop-rc), 1, stdout);
}


/*
 * <?theme cwd?>
 */
void
putcwd()
{
    char *eop = strrchr(document, '/');
    char *p;

    printf("/~%s/", username);
    if (eop) {
	fwrite(document, (int)(eop-document), 1, stdout);
	putchar('/');
    }
}

/*
 * <?theme root?>
 */
void
putroot()
{
    int up = 0;
    char *p;

    for (p=document; *p; ++p)
	if ( (*p == '/') && (p[1] != '/')) up++;

    while (up-- > 1)
	printf("../");
}


/*
 * <?theme sccs?>
 */
void
putsccs()
{
    putvariable("sccs");
}


/*
 * read the web page, picking up theme directives off the top of the
 * file.
 */
void
predigest(FILE *page)
{
    off_t start;
    char line[2000];
    char *p;

#define MATCHES(h,p)	(strncasecmp((h),(p),strlen(p)) == 0)

    while (1) {
	start = ftell(page);
	fgets(line, sizeof line, page);
	strtok(line,"\r\n");
	if (MATCHES(line,"sccs:"))
	    stash("sccs", line+5);
	else if (MATCHES(line, "title:"))
	    stash("title", line+6);
	else if (MATCHES(line, "meta:")) {
	    p = line+5;
	    szmeta += (strlen(p) + 2);
	    if (meta) {
		meta = realloc(meta, szmeta);
		strcat(meta, p);
	    }
	    else {
		meta = malloc(szmeta);
		strcpy(meta, p);
	    }
	}
	else if (MATCHES(line, "set ") && (p = strstr(line, ":="))) {
	    char *var = line + 4;

	    *p = 0;	// terminate the variable and skip over the rest of
	    p += 2;	// the := and any following whitespace.
	    while (isspace(*p)) ++p;

	    stash(var, p);
	}
	else {
	    fseek(page, start, SEEK_SET);
	    return;
	}
    }
}


/*
 * <?theme body?>
 */
void
putpagebody(FILE *page)
{
    int level = 0;
    int i;
    register c;
    off_t start;

    start = ftell(page);
    while ( (c = fgetc(page)) != EOF ) {
	if (c == marker[level]) {
	    if (marker[++level] == 0) {
		action(page);
		level = 0;
	    }
	}
	else {
	    for (i=0; i < level; i++)
		putchar(marker[i]);
	    level = 0;
	    putchar(c);
	}
    }
    fseek(page, start, SEEK_SET);
}


/*
 * putpagebody() wrapped to prevent body loops
 */
void
putbodyaction(FILE *page)
{
    time_t now;

    time(&now);
    if (doc == 0) {
	doc++;
	printf("<!-- %s wrapped by theme %s\n", url, "1.17");
	printf("  -- (http://www.pell.portland.or.us/~orc/Src/theme)\n");
	printf("  -- on %s", ctime(&now));
	printf("  -->\n");
	putpagebody(page);
	printf("<!-- end of %s -->\r\n", url);
	doc--;
    }
}



/*
 * process a <?theme XXX?> command
 */
struct op {
    char *keyword;
    void (*action)(FILE*);
} operations[] = {
    { "title", putpagetitle },
    { "meta",  putpagemeta  },
    { "body",  putbodyaction },
    { "rc",    putrc },
    { "cwd",   putcwd },
    { "root",  putroot },
    { "sccs",  putsccs },
};
#define NR_OPS	(sizeof operations/sizeof operations[0])

void
action(FILE *input)
{
    int i=0, st=0;
    char what[80];
    register c;

    while ((c = fgetc(input)) != EOF) {
	if (c == '?' && (st == 0) )
	    st = 1;
	else if ( (c == '>') && (st == 1) )
	    break;
	else
	    st = 0;
	if (i < sizeof(what)-2)
	    what[i++] = c;
    }
    what[--i] = 0;

    if (what[0] == '$')
	putvariable(1+what);
    else {
	for (i=0; i < NR_OPS; i++)
	    if (strcasecmp(what, operations[i].keyword) == 0) {
		(*operations[i].action)(page);
		break;
	    }
    }
}


void
type(FILE *f)
{
    char *content;

#if HAVE_MMAP
    /* mmap is far groovier than boring old read/write
     */
    struct stat st;
    char *p;

    if (fstat(fileno(f), &st) != 0)
	falloverdead(404, "fstat: %s", strerror(errno));

    if ((p = mmap(0, st.st_size, PROT_READ, MAP_SHARED, fileno(f), 0)) == 0)
	falloverdead(404, "mmap: %s", strerror(errno));

    content = magic_buffer(poof, p, st.st_size);

    if (avec_headers) {
	printf("Content-Size: %lu\r\n", st.st_size);
	printf("Content-Type: %s\r\n", content ? content : "text/plain");
    }
    write(fileno(stdout), p, st.st_size);
# if HAVE_MUNMAP
    munmap(p, st.st_size);
# endif
#else
    char blk[10240];
    int size;
    int first = 1;

    while ( (size=fread(blk, 1, sizeof blk, f)) > 0) {
	if (first) {
	    first = 0;
	    content = magic_buffer(poof, blk, size);
	    if (avec_headers) {
		printf("Content-Type: %s\r\n\r\n",
			content?content:"text/plain");
	    }
	    magic_close(poof);
	}
	fwrite(blk, 1, size, stdout);
    }
#endif
}


goodfortheme(char *extension)
{
    char *p;

    for (p = strtok(themable, ":"); p; p = strtok(0,":")) {
#if 0
	syslog(LOG_ERR, "look at extension (%s vs %s)", p, extension);
#endif
	if (strcasecmp(extension, p) == 0)
	    return 1;
    }
    return 0;
}


/*
 * theme
 */
main(int argc, char **argv)
{
    register c;
    int i;
    FILE *theme;
    char *p, *q;
    char *rest;
    struct passwd *user;
    struct stat st;

    openlog("Theme", 0, LOG_USER);
    if ( (poof = magic_open(MAGIC_MIME)) == 0)
	falloverdead(500, "Error opening /etc/magic");
    magic_load(poof,0);

    if (argc <= 1) {
	if ( (p = path = getenv("PATH_INFO")) == 0)
	    falloverdead(500, "$PATH_INFO not defined");
    }
    else {
	avec_headers = 0;
	p = path = argv[1];
    }


    url = strdup(p);
    if (*p == '/') ++ p;

    if (*p == '~') {
	++p;
	for (rest=p; *rest && *rest != '/'; ++rest)
	    ;
	if (*rest)
	    *rest++ = 0;
	else
	    falloverdead(404, "%s: (rest) No such file or directory", path);

	if ( (user = getpwnam(p)) == 0)
	    falloverdead(404, "%s: (%s) No such file or directory", path, p);

	if (chdir(user->pw_dir) != 0 || chdir(USER_DIR) != 0)
	    falloverdead(404, "%s: %s", user->pw_dir, strerror(errno));
	chroot(user->pw_dir) && chroot(USER_DIR);

	username = strdup(user->pw_name);

	if (stat(rest, &st) != 0)
	    falloverdead(404, "%s: %s", rest, strerror(errno));
	if ( (st.st_mode & S_IFMT) == S_IFDIR) {
	    document = malloc(strlen(rest) + 11);
	    sprintf(document, "%s/index.html", rest);
	}
	else
	    document = strdup(rest);


	if ( (page = fopen(document, "r")) == 0)
	    falloverdead(404, "%s: %s", rest, strerror(errno));

	if ( (p = strrchr(document, '.')) && goodfortheme(p)) {
	    predigest(page);
	    rc = malloc(strlen(rest) + strlen("page.theme") + 2);
	    while (1) {
		if ((q = strrchr(rest, '/')) == 0)
		falloverdead(500, "No theme file found");
		*q = 0;
		sprintf(rc, "%s/page.theme", rest);
		if (theme = fopen(rc, "r"))
		    break;
	    }

	    if (avec_headers)
		printf("Content-Type: text/html\r\n\r\n");
	    putpagebody(theme);
	    fclose(theme);
	}
	else
	    type(page);

	fclose(page);
    }
    else {
	falloverdead(500, "Themes can only be applied to personal pages");
    }
}
