Plan 9 from Bell Labs’s /usr/web/sources/contrib/nemo/sys/src/cmd/ip/httpd/group.c

Copyright © 2021 Plan 9 Foundation.
Distributed under the MIT License.
Download the Plan 9 distribution.


/*
 * Web discussion groups.
 * The group is identified by an argument.
 * Each group is kept as a flat directory.
 * There we keep a file per article.
 * Each file is made of a series of sections.
 * The first section is considered the article
 * The following ones are the replies.
 * The Id determines to which post they are replying.
 * Number | Id.Number
 * title
 * Author
 * body lines...
 * \a (ascii 07 / bell)
 */


#include <u.h>
#include <libc.h>
#include <bio.h>
#include <ctype.h>
#include "httpd.h"
#include "httpsrv.h"
#include "article.h"

enum {
	Nfront	= 10,	// max # in front page
};


typedef struct Req  Req;
struct Req{
	char*	group;
	char*	file;
	int	op;
	HSPairs*args;
};

typedef struct Opf Opf;
struct Opf {
	char	op;
	void	(*f)(Req*);
};

static	Hio*	hout;
static	Hio	houtb;
static	int	vermaj;
static	HConnect*hcon;

static	int	debug = 0;


static	char	Dfltreq[]= "o=i&g=dso";
static	char	Top[]	= "/groups";
static	char	Mark[]	= "DYNAMIC";

/* 
 * Strings seen in output, to help with localization.
 */
static	char	Msearchresults[] = "Resultados de la búsqueda";
static	char	Mreturn[] = "Volver.";
static	char	Msendnew[]= "Enviar nuevo articulo";
static	char	Mreply[] = "Responder";
static	char	Mreplyto[]= "Responder a...";
static	char	Manswers[] = "resp.";
static	char	Mgoonreading[] = "Seguir leyendo otros";
static	char	Mgoback[] = "Volver atras";
static	char	Mreadmore[] = "Leer mas";
static	char	Mreturntoindex[] = "Volver a la portada";
static	char	Mwrote[] = "dice...";
static	char	Mby[] = "Por";
static	char	Mrequiredfieldmissing[] =
		"No has escrito en un campo requerido.\n"
		"Usa back en tu navegador e intenta de nuevo\n";
static	char	Mclickheretoreturn[] = "<p>Hecho. Pulsa "
		"<a href=\"/magic/group?o=i&g=%s\">aquí</a> "
		"para volver.\n";
static	char	Mbodywithlonglines[]= 
	"Tienes lineas muy largas. Usa back en tu navegador\n"
	"y usa return para que tu mensaje tenga lineas\n"
	"que se puedan leer bien (max 80 caracteres).\n";

static void
badreq(void)
{
	hfail(hcon, HBadReq);
	exits(nil);
}

static void
notfound(char* o)
{
	hfail(hcon, HNotFound, o);
	exits(nil);
}

static void
failure(void)
{
	hfail(hcon, HTempFail);
	exits(nil);
}

static void
headers(char* title)
{
	if(vermaj){
		hokheaders(hcon);
		hprint(hout, "Content-type: text/html; charset=UTF-8\r\n");
		hflush(hout);
	}
	if (title != nil)
		hprint(hout, "<head><title>%s</title></head><body>\n", title);
}

static void
notice(char* fmt, ...)
{
	va_list arg;
	char buf[1024], *out;

	va_start(arg, fmt);
	out = vseprint(buf, buf+sizeof(buf), fmt, arg);
	va_end(arg);
	*out = 0;
	headers("notice");
	hprint(hout, "%s", buf);
	hprint(hout, "</body>\n");
}

static char*
getarg(Req* r, char arg, int must)
{
	HSPairs*p;

	for (p = r->args; p != nil; p = p->next){
		if (p->s[0] == arg)
			return p->t;
	}
	if (must)
		badreq();
	return nil;
}

static void
parsereq(char* s, Req* r)
{
	char*	v;

	memset(r, 0, sizeof(Req));
	r->args = hparsequery(hcon, s);

	v = getarg(r, 'o', 1);
	r->op = *v;
	v = getarg(r, 'g', 1);
	r->group = strdup(v);
}

static void
outfilehdr(Biobuf* bin)
{
	char*	ln;

	while(ln = Brdstr(bin, '\n', 1)){
		if (strcmp(ln, Mark) == 0){
			free(ln);
			return;
		}
		hprint(hout, "%s\n", ln);
		free(ln);
	}
}

static void
outfiletail(Biobuf* bin)
{
	char*	ln;

	while(ln = Brdstr(bin, '\n', 1)){
		hprint(hout, "%s\n", ln);
		free(ln);
	}
}

static void
getpostintro(Post* p, char* intro, int max, int* more)
{
	char*	e;
	char*	w;
	int	l;

	e = strecpy(intro, intro+max-4, p->body);
	l = e - intro;
	*more = (p->body[l] != 0);
	if (*more){
		w = strrchr(intro, ' ');
		if (w)
			e = w;
		strcpy(e, "...");
	}
}

static char* Mon[] = {
	"Ene", "Feb", "Mar", "Abr", "May", "Jun",
	"Jul", "Ago", "Sep", "Oct", "Nov", "Dic"
};

static char*
artdate(ulong mt)
{
	Tm*	tm;
	static char str[40];

	if (mt == 0)
		return "";
	tm = localtime(mt);
	seprint(str, str+40, "(%d-%s)", tm->mday, Mon[tm->mon]);
	return str;
}

static Biobuf*
bopenfile(char* group, char* art, ulong* mt)
{
	char*	file;
	Biobuf*	ib;
	Dir*	d;

	file = smprint("%s/%s/%s", Top, group, art);
	if (strstr(file, "..")){
		free(file);
		badreq();
	}
	if (mt != nil){
		*mt = 0;
		d = dirstat(file);
		if (d != nil){
			*mt = d->mtime;
			free(d);
		}
	}
	ib = Bopen(file, OREAD);
	free(file);
	return ib;
}

static int
outpostintro(char* g, char* file, int verbose)
{
	Biobuf*	bin;
	Post*	p;
	int	n;
	char	intro[120];
	int	more;
	char*	date;
	ulong	mt;

	bin = bopenfile(g, file, &mt);
	if (bin == nil)
		return 0;
	if (debug)
		fprint(2, "%s/%s\n", g, file);
	p = readarticle(bin, &n);
	if (p == nil){
		Bterm(bin);
		return 0;
	}
	date = artdate(mt);
	if(debug)
		fprint(2, "article %s\n", p->title);
	switch(verbose){
	case 0:
		hprint(hout,"<b><a href=\"/magic/group?o=a&g=%s&a=%s\">",
			g, file);
		hprint(hout, "%s</a> (%s; %d posts)</b><br>\n", p->title, p->author, n);
		break;
	case 1:
		hprint(hout,"<p> <a href=\"/magic/group?o=a&g=%s&a=%s\">",
			g, file);
		hprint(hout, "<font size=+1><b>%s</b> %s</a> ", p->title, date);
		hprint(hout, "<font size=-1><b>(%s %s)</b><br>\n", Mby, p->author);
		break;
	default:
		hprint(hout, "<p><font size=+1><hr><b>%s</b> %s", p->title, date);
		hprint(hout, "<br><font size=-1><b>%s %s</b> <br>\n", Mby, p->author);
		getpostintro(p, intro, sizeof(intro), &more);
		hprint(hout, "<font size=+0>%s<br>\n", intro);
		if (n > 1)
			hprint(hout,
				"<a href=\"/magic/group?o=a&g=%s&a=%s\">Leer "
				"mas</a> (%d %s.)<p>\n", g, file, n-1, Manswers);
		else
			hprint(hout,
				"<a href=\"/magic/group?o=a&g=%s&a=%s\">%s"
				"</a><p>\n",g, file, Mreadmore);
	}
	closepost(p);
	Bterm(bin);
	return 1;
}

static char*
readfile(char* fname)
{
	Dir*	d;
	vlong	len;
	int	fd;
	char*	buf;

	fd = open(fname, OREAD);
	if (fd < 0)
		return nil;
	d = dirfstat(fd);
	assert (d != nil);
	len = d->length;
	free(d);
	buf = malloc(len +1);
	buf[0] = 0;
	readn(fd, buf, len);
	close(fd);
	buf[len] = 0;
	return buf;
}

static int
idxsort(void* a, void* b)
{
	Dir* da = a;
	Dir* db = b;

	if (da->name[0] != 'A')
		return 1;
	return (int)(da->mtime - db->mtime);
}

static char*
readindex(Req* req, char*** entsp, int* nentsp)
{
	Dir*	d;
	char**	ents;
	int	fd;
	char*	buf;
	char*	dir;
	int	nd;
	long	l;
	int	i, j;
	char*	s;

	/* Experiment, read all article files, and
	 * sort by mtime.
	 */
	if (strstr(req->group, ".."))
		notfound("..");
	dir = smprint("%s/%s", Top, req->group);
	fd = open(dir, OREAD);
	if (fd < 0)
		failure();
	nd = dirreadall(fd, &d);
	close(fd);
	if (nd <= 0){
		*entsp = nil;
		*nentsp = 0;
		return nil;
	}
	qsort(d, nd, sizeof(d[0]), idxsort);
	l = 0;
	for (i = 0; i < nd; i++){
		if (d[i].name[0] != 'A')
			continue;
		l += strlen(d[i].name) + 2;
	}
	s = buf = malloc(l);
	ents=malloc(sizeof(char*)*(nd+1));
	if (buf == nil){
		free(d);
		failure();
	}
	for (i = j = 0; i < nd; i++){
		//hprint(hout, "debug: art = %s\n<br>", d[i].name);
		if (d[i].name[0] == 'A'){
			ents[j++] = s;
			s = strecpy(s, buf+l, d[i].name);
			*s++ = 0;
		}
	}
	*entsp = ents;
	*nentsp= j;
	free(d);
	return buf;
}

static void
getidxreq(Req* req)
{
	static char prevfmt[] = "<a href=\"/magic/group?o=i&g=%s&S=%d\">%s</a><p>\n";
	static char nextfmt[] = "<a href=\"/magic/group?o=i&g=%s&S=%d\">%s</a><p>\n";
	char*	idx;
	char**	ents;
	int	nents;
	char*	buf;
	Biobuf*	ib;
	int	i;
	int	start;
	int	n;
	char*	s;

	idx = smprint("%s/%s/index.html", Top, req->group);
	start = 0;
	s = getarg(req, 'S', 0);
	if (s != nil)
		start=atoi(s);
	ib = Bopen(idx, OREAD);
	if (ib == nil)
		notfound("index");
	headers(nil);
	outfilehdr(ib);
	buf = readindex(req, &ents, &nents);
	n = 0;
	for (i = nents-1; i >= 0; i--){
		if (start != 0 && n == start){
			hprint(hout, prevfmt , req->group,
				((start - Nfront) < 0 ? 0 : (start - Nfront)), Mgoback);
		}
		if (n >= start && n < start+Nfront)
			outpostintro(req->group, ents[i], 2);
		if (n == start+Nfront){
			hprint(hout, nextfmt, req->group,
				start + Nfront, Mgoonreading);
			break;
		}
		n++;
	}
	outfiletail(ib);
	Bterm(ib);
	free(idx);
	free(ents);
	free(buf);
}

static void
outpost(int tab, char* g, char* f, Post* p)
{
	char	tabs[Nids+1];
	char	*np;
	char	*sp;
	char*	s;
	char*	titlefmt= "%s<font size=+2><b>%s</b><font size=-2>\n<p>";
	char*	authfmt= "%s<font size=+1><b>%s %s</b><font size=-1><br>\n";
	char*	otherfmt = "%s<font size=+1><b>%s %s</b><font size=-1><br>\n";
	char*	replfmt = "%s<a href=\"/magic/group?o=r&g=%s&a=%s&"
				"i=%s\">%s</a>\n";

	memset(tabs, '\t', sizeof(tabs));
	tabs[tab] = 0;

	if (tab <= 0){
		if (p->title[0] != 0)
			hprint(hout, titlefmt, tabs, p->title);
		hprint(hout, authfmt, tabs, Mby, p->author);
	} else
		hprint(hout, otherfmt, tabs, p->author, Mwrote);
	sp = p->body;
	if (p->body[0] != 0){
		while(sp != nil){
			np = strchr(sp, '\n');
			hprint(hout, "%s", tabs);
			if (np == nil)
				hprint(hout, "%s\n", sp);
			else{
				hwrite(hout, sp, np-sp);
				hprint(hout, "\n");
				np++;
			}
			sp = np;
		}
	}
	if (g != nil && f != nil){
		s = id2str(p->ids, p->nids);
		hprint(hout, replfmt, tabs, g, f, s, Mreply);
	}
	hprint(hout, "<hr>\n");
}

static void
outarticle(int tab, char* g, char* f, Post* p)
{
	int	i;

	outpost(tab, g, f, p);
	for (i = 0; i < p->nsons; i++)
		outarticle(tab+1, g, f, p->sons[i]);
}

static Post*
openarticle(char* group, char* art)
{
	Post*	p;
	Biobuf*	ib;
	ulong	mt;

	ib = bopenfile(group, art, &mt);
	p = readarticle(ib, nil);
	if (p == nil){
		Bterm(ib);
		notfound(art);
	}
	Bterm(ib);
	return p;
}

static void
getartreq(Req* req)
{
	Post*	p;
	Biobuf*	ib;

	ib = bopenfile(req->group, "article.html", nil);
	if (ib == nil)
		notfound("article.html");
	req->file = getarg(req, 'a', 1);
	// Could locate previous and following article in index
	// to output <next><prev> links near the bottom.
	p = openarticle(req->group, req->file);
	headers(nil);
	outfilehdr(ib);
	outarticle(0, req->group, req->file, p);
	hprint(hout, "<p><a href=\"/magic/group?o=i&g=%s\">%s</a>\n",
		req->group, Mreturntoindex);
	outfiletail(ib);
	Bterm(ib);
}

static void
outreplyform(Req* r, Post* p, char* form)
{
	Biobuf*	ib;
	char*	id;

	ib = bopenfile(r->group, form, nil);
	if (ib == nil)
		notfound(form);
	outfilehdr(ib);
	if (p == nil || r->file == nil){
		hprint(hout, "<h1>%s</h1>\n", Msendnew);
		hprint(hout,
			"<form action=\"/magic/group?o=P&g=%s\" "
			"method=\"post\">\n<p>",
		   	r->group);
	} else {
		hprint(hout, "<h1>%s</h1>\n<hr><pre>", Mreplyto);
		outpost(0, nil, nil, p);
		id = id2str(p->ids, p->nids);
		hprint(hout,
			"</pre><p><form action=\"/magic/group?o=P&g=%s&a=%s&i=%s\" "
			"method=\"post\">\n<p>",
		   	r->group, r->file, id);
		free(id);
	}
	outfiletail(ib);

	Bterm(ib);
}

static void
replyreq(Req* req)
{
	Post*	p;
	Post*	top;
	char*	id;
	int	ids[Nids];
	int	nids;
	Biobuf*	ib;
	char*	form;

	p = nil;
	req->file = getarg(req, 'a', 0);
	if (req->file == nil)
		form = "post.html";
	else {
		form = "reply.html";
		id = getarg(req, 'i', 0);
		if (id == nil)
			badreq();
		nids = parseid(id, ids, Nids);
		ib = bopenfile(req->group, req->file, nil);
		top = readarticle(ib, nil);
		if (top == nil)
			notfound(req->file);
		Bterm(ib);
		p = lookup(top, ids, nids);
		if (p == nil){
			closepost(top); // BUG: leak: sons
			badreq();
		}
	}
	headers(nil);
	outreplyform(req, p, form);
}

static void
putpost(Req* r, Post* np)
{
	static char buf[Maxpost];
	char*	e;
	Post*	p;
	int	fd;
	char*	fname;
	char	name[40];

	if (r->file != nil){
		p = openarticle(r->group, r->file);
		p = lookup(p, np->ids, np->nids);
		if (p == nil)
			notfound("previous article");
		closepost(p); // BUG: leaks sons
		if (np->nids == Nids)
			failure();
		np->ids[np->nids++] = truerand()%10000;
		fname = smprint("%s/%s/%s", Top, r->group, r->file);
		if (strstr(fname, ".."))
			badreq();
		fd = open(fname, OWRITE);
		free(fname);
	} else {
		fname = smprint("%s/%s/INDEX", Top, r->group);
		fd = open(fname, OWRITE);
		if (fd < 0)
			notfound("INDEX");
		free(fname);
		seprint(name, name+sizeof(name), "A%08uld",  truerand()%10000000);
		fname = smprint("%s/%s/%s", Top, r->group, name);
		if (strstr(fname, ".."))
			badreq();
		seek(fd, 0, 2);
		fprint(fd, "%s\n", name);
		close(fd);
		//fprint(2, "creating %s\n", fname);
		fd = create(fname, OWRITE, DMAPPEND|0666);
		free(fname);
		np->ids[0] = truerand()%10000;
		np->nids = 1;
	}
	if (fd < 0)
		notfound("post");
	seek(fd, 0, 2);
	e = seprintpost(buf, buf+sizeof(buf), np);
	write(fd, buf, e - buf);
	close(fd);
}

static int
checkbodyok(char* body)
{
	char*	ln;

	while(*body){
		ln = strchr(body+1, '\n');
		if (ln == nil)
			ln = body+strlen(body);
		if (ln - body > 80){
			notice(Mbodywithlonglines);
			return 0;
		}
		body = ln;
	}
	return 1;
}

static void
putreplyreq(Req* r)
{
	Post*	p;
	char*	id;

	p = newpost();
	id = getarg(r, 'i', 0);
	r->file  = getarg(r, 'a', 0);
	p->title = getarg(r, 'T', 0);
	p->author= getarg(r, 'A', 1);
	p->body  = getarg(r, 'B', 1);
	if(r->file == nil){	// new article
		if (!p->title)
			badreq();
		if (!p->title[0]){
			notice(Mrequiredfieldmissing);
			return;
		}
	}
	if (!p->author[0] || !p->body[0]){
		notice(Mrequiredfieldmissing);
		return;
	}
	if (!checkbodyok(p->body))
		return;
	if (id == nil){
		if (r->file != nil)
			badreq();
		p->nids = 0;
	} else{
		if (r->file == nil)
			badreq();
		p->nids = parseid(id, p->ids, Nids);
	}
	if (p->title == nil)
		p->title = strdup("");
	if (p->body == nil)
		p->body = strdup("");
	putpost(r, p);
	notice(Mclickheretoreturn, r->group);
}

static void
searchreq(Req* r)
{
	char*	words[10];
	int	nwords;
	char*	arg;
	char**	ents;
	int	nents;
	char*	buf;
	char*	fbuf;
	int	i, j;
	char*	fname;

	// All the Post has been parsed,
	// but for the extra from fields.

	arg = getarg(r, 'w', 1);
	nwords = tokenize(arg, words, nelem(words));
	buf = readindex(r, &ents, &nents);

	for (i = 0; i < nents; i++){
		fname = smprint("%s/%s/%s", Top, r->group, ents[i]);
		fbuf = readfile(fname);
		free(fname);
		if (fbuf != nil){
			for (j = 0; j < nwords; j++){
				if (cistrstr(fbuf, words[j]) == 0){
					ents[i] = nil;
					break;
				}
			}
			free(fbuf);
		} else
			ents[i] = nil;
	}
	headers("search");
	hprint(hout, "<body bgcolor=\"white\">\n");
	hprint(hout, "<h1>%s</h1>\n<hr><p>\n", Msearchresults);
	for (i = 0; i < nents; i++){
		if (ents[i] == nil)
			continue;
		outpostintro(r->group, ents[i], 2);
	}
	hprint(hout, "<p><hr><p><a href=\"/magic/group?o=i&g=%s\">%s"
		"</a></body>\n", r->group, Mreturn);
	free(buf);
	free(ents);
}

static Opf	ops[] = {
	{ 'i',	getidxreq },
	{ 'a',	getartreq },
	{ 'r',	replyreq },
	{ 'P',	putreplyreq },
	{ 's',	searchreq },
};

static void
request(char* req)
{
	Req	r;
	int	i;

	if (req == nil)
		req = Dfltreq;
	parsereq(req, &r);
	if (debug)
		fprint(2, "op %d, group %s\n", r.op, r.group);
	for (i = 0; i < nelem(ops); i++)
		if (r.op == ops[i].op){
			ops[i].f(&r);
			break;
		}
	if (i == nelem(ops))
		badreq();
}

void
main(int argc, char **argv)
{
	Hio*	hin;
	char*	t;
	char*	s;
	char*	opts;
	fmtinstall('H', httpfmt);
	fmtinstall('U', hurlfmt);

	if (debug)
		dup(open("/dev/cons", OWRITE), 2);
	else
		close(2);

	hcon = init(argc, argv);


	hout = &hcon->hout;
	if(hparseheaders(hcon, HSTIMEOUT) < 0)
		exits("failed");
	hcon->head.closeit = 1;
	if(strcmp(hcon->req.meth, "GET") != 0 && strcmp(hcon->req.meth, "HEAD") != 0)
	if(strcmp(hcon->req.meth, "POST") != 0){
		hunallowed(hcon, "GET, HEAD, PUT");
		exits("not allowed");
	}
	if(hcon->head.expectother){
		hfail(hcon, HExpectFail, nil);
		exits("failed");
	}
	if(hcon->head.expectcont){
		hprint(hout, "100 Continue\r\n");
		hprint(hout, "\r\n");
		hflush(hout);
	}
	if (strcmp(hcon->req.meth, "POST") == 0){
		s = nil;
		hin = hbodypush(&hcon->hin,
			hcon->head.contlen, hcon->head.transenc);
		if(hin != nil){
			alarm(15*60*1000);
			s = hreadbuf(hin, hin->pos);
			alarm(0);
		}
		if(s == nil){
			hfail(hcon, HBadReq, nil);
			exits("failed");
		}
		t = strchr(s, '\n');
		if(t != nil)
			*t = '\0';
		if (hcon->req.search != nil)
			opts = smprint("%s&%s", hcon->req.search, s);
		else
			opts = s;
	} else
		opts = hcon->req.search;
	vermaj = hcon->req.vermaj;

	truerand();	// open /dev/random before bind.
	bind("/usr/web", "/", MREPL);
	request(opts);
	hlflush(hout);
	hclose(hout);
	writelog(hcon, "200 group %ld %ld\n", hout->seek, hout->seek);
	exits(nil);
}

Bell Labs OSI certified Powered by Plan 9

(Return to Plan 9 Home Page)

Copyright © 2021 Plan 9 Foundation. All Rights Reserved.
Comments to webmaster@9p.io.