/*
 * Accept new wiki pages or modifications to existing ones via POST method.
 *
 * Talks to the server at /srv/wiki.service.
 */
#include <u.h>
#include <libc.h>
#include <bio.h>
#include "httpd.h"
#include "httpsrv.h"

#define LOG "wiki"

HConnect *hc;
HSPriv *hp;


/* go from possibly-latin1 url with escapes to utf */
char *
_urlunesc(char *s)
{
	char *t, *v, *u;
	Rune r;
	int c, n;

	/* unescape */
	u = halloc(hc, strlen(s)+1);
	for(t = u; c = *s; s++){
		if(c == '%'){
			n = s[1];
			if(n >= '0' && n <= '9')
				n = n - '0';
			else if(n >= 'A' && n <= 'F')
				n = n - 'A' + 10;
			else if(n >= 'a' && n <= 'f')
				n = n - 'a' + 10;
			else
				break;
			r = n;
			n = s[2];
			if(n >= '0' && n <= '9')
				n = n - '0';
			else if(n >= 'A' && n <= 'F')
				n = n - 'A' + 10;
			else if(n >= 'a' && n <= 'f')
				n = n - 'a' + 10;
			else
				break;
			s += 2;
			c = r*16+n;
		}
		*t++ = c;
	}
	*t = 0;

	/* latin1 heuristic */
	v = halloc(hc, UTFmax*strlen(u) + 1);
	s = u;
	t = v;
	while(*s){
		/* in decoding error, assume latin1 */
		if((n=chartorune(&r, s)) == 1 && r == 0x80)
			r = *s;
		s += n;
		t += runetochar(t, &r);
	}
	*t = 0;

	return v;
}

enum
{
	MaxLog		= 100*1024,		/* limit on length of any one log request */
};

static int
dangerous(char *s)
{
	if(s == nil)
		return 1;

	/*
	 * This check shouldn't be needed;
	 * filename folding is already supposed to have happened.
	 * But I'm paranoid.
	 */
	while(s = strchr(s,'/')){
		if(s[1]=='.' && s[2]=='.')
			return 1;
		s++;
	}
	return 0;
}

char*
unhttp(char *s)
{
	char *p, *r, *w;

	if(s == nil)
		return nil;

	for(p=s; *p; p++)
		if(*p=='+')
			*p = ' ';
	s = _urlunesc(s);

	for(r=w=s; *r; r++){
		if(*r != '\r')
			*w++ = *r;
	}
	*w = '\0';
	return s;
}

void
mountwiki(HConnect *c, char *service)
{
	char buf[128];
	int fd;

	/* already in (possibly private) namespace? */
	snprint(buf, sizeof buf, "/mnt/wiki.%s/new", service);
	if (access(buf, AREAD) == 0){
		if (bind(buf, "/mnt/wiki", MREPL) < 0){
			syslog(0, LOG, "%s bind /mnt/wiki failed: %r",
				hp->remotesys);
			hfail(c, HNotFound);
			exits("bind /mnt/wiki failed");
		}
		return;
	}

	/* old way: public wikifs from /srv */
	snprint(buf, sizeof buf, "/srv/wiki.%s", service);
	if((fd = open(buf, ORDWR)) < 0){
		syslog(0, LOG, "%s open %s failed: %r", buf, hp->remotesys);
		hfail(c, HNotFound);
		exits("failed");
	}
	if(mount(fd, -1, "/mnt/wiki", MREPL, "") < 0){
		syslog(0, LOG, "%s mount /mnt/wiki failed: %r", hp->remotesys);
		hfail(c, HNotFound);
		exits("failed");
	}
	close(fd);
}

char*
dowiki(HConnect *c, char *title, char *author, char *comment, char *base, ulong version, char *text)
{
	int fd, l, n, err;
	char *p, tmp[256];
int i;

	if((fd = open("/mnt/wiki/new", ORDWR)) < 0){
		syslog(0, LOG, "%s open /mnt/wiki/new failed: %r", hp->remotesys);
		hfail(c, HNotFound);
		exits("failed");
	}

i=0;
	if((i++,fprint(fd, "%s\nD%lud\nA%s (%s)\n", title, version, author, hp->remotesys) < 0)
	|| (i++,(comment && comment[0] && fprint(fd, "C%s\n", comment) < 0))
	|| (i++,fprint(fd, "\n") < 0)
	|| (i++,(text[0] && write(fd, text, strlen(text)) != strlen(text)))){
		syslog(0, LOG, "%s write failed %d %ld fd %d: %r", hp->remotesys, i, strlen(text), fd);
		hfail(c, HInternal);
		exits("failed");
	}

	err = write(fd, "", 0);
	if(err)
		syslog(0, LOG, "%s commit failed %d: %r", hp->remotesys, err);

	seek(fd, 0, 0);
	if((n = read(fd, tmp, sizeof(tmp)-1)) <= 0){
		if(n == 0)
			werrstr("short read");
		syslog(0, LOG, "%s read failed: %r", hp->remotesys);
		hfail(c, HInternal);
		exits("failed");
	}

	tmp[n] = '\0';

	p = halloc(c, l=strlen(base)+strlen(tmp)+40);
	snprint(p, l, "%s/%s/%s.html", base, tmp, err ? "werror" : "index");
	return p;
}


void
main(int argc, char **argv)
{
	Hio *hin, *hout;
	char *s, *t, *p, *f[10];
	char *text, *title, *service, *base, *author, *comment, *url;
	int i, nf;
	ulong version;

	hc = init(argc, argv);
	hp = hc->private;

	if(dangerous(hc->req.uri)){
		hfail(hc, HSyntax);
		exits("failed");
	}

	if(hparseheaders(hc, HSTIMEOUT) < 0)
		exits("failed");
	hout = &hc->hout;
	if(hc->head.expectother){
		hfail(hc, HExpectFail, nil);
		exits("failed");
	}
	if(hc->head.expectcont){
		hprint(hout, "100 Continue\r\n");
		hprint(hout, "\r\n");
		hflush(hout);
	}

	s = nil;
	if(strcmp(hc->req.meth, "POST") == 0){
		hin = hbodypush(&hc->hin, hc->head.contlen, hc->head.transenc);
		if(hin != nil){
			alarm(15*60*1000);
			s = hreadbuf(hin, hin->pos);
			alarm(0);
		}
		if(s == nil){
			hfail(hc, HBadReq, nil);
			exits("failed");
		}
		t = strchr(s, '\n');
		if(t != nil)
			*t = '\0';
	}else{
		hunallowed(hc, "GET, HEAD, PUT");
		exits("unallowed");
	}

	if(s == nil){
		hfail(hc, HNoData, "wiki");
		exits("failed");
	}

	text = nil;
	title = nil;
	service = nil;
	author = "???";
	comment = "";
	base = nil;
	version = ~0;
	nf = getfields(s, f, nelem(f), 1, "&");
	for(i=0; i<nf; i++){
		if((p = strchr(f[i], '=')) == nil)
			continue;
		*p++ = '\0';
		if(strcmp(f[i], "title")==0)
			title = p;
		else if(strcmp(f[i], "version")==0)
			version = strtoul(unhttp(p), 0, 10);
		else if(strcmp(f[i], "text")==0)
			text = p;
		else if(strcmp(f[i], "service")==0)
			service = p;
		else if(strcmp(f[i], "comment")==0)
			comment = p;
		else if(strcmp(f[i], "author")==0)
			author = p;
		else if(strcmp(f[i], "base")==0)
			base = p;
	}

	syslog(0, LOG, "%s post s %s t '%s' v %ld a %s c %s b %s t 0x%p",
		hp->remotesys, service, title, (long)version, author, comment, base, text);

	title = unhttp(title);
	comment = unhttp(comment);
	service = unhttp(service);
	text = unhttp(text);
	author = unhttp(author);
	base = unhttp(base);

	if(title==nil || version==~0 || text==nil || text[0]=='\0' || base == nil 
	|| service == nil || strchr(title, '\n') || strchr(comment, '\n')
	|| dangerous(service) || strchr(service, '/') || strlen(service)>20){
		syslog(0, LOG, "%s failed dangerous", hp->remotesys);
		hfail(hc, HSyntax);
		exits("failed");
	}

	syslog(0, LOG, "%s post s %s t '%s' v %ld a %s c %s",
		hp->remotesys, service, title, (long)version, author, comment);

	if(strlen(text) > MaxLog)
		text[MaxLog] = '\0';

	mountwiki(hc, service);
	url = dowiki(hc, title, author, comment, base, version, text);
	hredirected(hc, "303 See Other", url);
	exits(nil);
}
