/* :ts=4
 * Implement the Usenet control messages, as per RFC 1036 (nee 850).
 * These are fairly infrequent and can afford to be done by
 * separate programs.  They are:
 *
 * cancel message-ID	restricted to Sender: else From: or root, in theory
 * ihave message-ID-list remotesys	generate a sendme from message-ID-list
 * sendme message-ID-list remotesys	send articles named to remotesys
 * (ihave/sendme is semi-documented in the RFCs, kludgey and broken in B2.10.)
 *
 * newgroup groupname	must be Approved:
 * rmgroup groupname	must be Approved:; allow some local control over this
 * sendsys		mail to Reply-To: else From:
 * senduuname		ditto
 * version		ditto
 *
 *	$Log$
 */

#include <stdio.h>
#include <ctype.h>
#ifdef unix
# include <sys/types.h>
#endif /* unix */

#include "libc.h"
#include "news.h"
#include "case.h"
#include "config.h"
#include "control.h"
#include "headers.h"
#include "article.h"
#include "caches.h"
#include "history.h"

#define NO_FILES ""
#define SUBDIR binfile("ctl")		/* holds shell scripts */
#ifdef SLOWCTLMATCH
#define OLDCNTRL "all.all.ctl"
#endif
#define SFXOLDCNTRL ".ctl"

/*
 * These are shell meta-characters, except for /, which is included
 * since it allows people to escape from the control directory.
 */
#define SHELLMETAS "<>|&;({$=*?[`'\"/"

/* imports from news */
extern statust snufffiles(); 
extern void ihave(), sendme();

/* forwards */
#ifndef AMIGA
	FORWARD statust cancelart();
	FORWARD void runctlmsg(), bombctlmsg();
#else
	FORWARD statust cancelart();
	FORWARD int runctlmsg();
#endif /* AMIGA */

/*
 * Implement control message specified in "art".
 * Because newgroup and rmgroup may modify the active file, for example,
 * we must flush in-core caches to disk first and reload them afterward.
 * We handle cancels in this process for speed and dbm access.
 * We handle ihave & sendme in this process for dbm access and
 * to work around syntax restrictions (<>).
 *
 * In future, one could pass header values to scripts as arguments or
 * in environment, as NEWS* variables, to save time in the scripts.
 */
void
ctlmsg(art)
register struct article *art;
{
	register char *inname = art->a_tmpf, *ctlcmd = art->h.h_ctlcmd;
	int pid, deadpid;
	int wstatus;
	static char nmcancel[] = "cancel ";
	static char nmihave[] = "ihave ";
	static char nmsendme[] = "sendme ";

	if (ctlcmd == NULL)
		ctlcmd = art->h.h_etctlcmd;
	if (ctlcmd == NULL)
		return;
	if (STREQN(ctlcmd, nmcancel, STRLEN(nmcancel))) {
		art->a_status |= cancelart(ctlcmd + STRLEN(nmcancel));
		return;
	}
	if (STREQN(ctlcmd, nmihave, STRLEN(nmihave))) {
		ihave(ctlcmd + STRLEN(nmihave), art);
		return;
	}
	if (STREQN(ctlcmd, nmsendme, STRLEN(nmsendme))) {
		sendme(ctlcmd + STRLEN(nmsendme), art);
		return;
	}
	/*
	 *	These "other" ctlmsgs are handled through the Amiga-tized
	 *	version of runctlmsg().  Basically I just call Execute()...
	 */
#ifndef AMIGA
	art->a_status |= synccaches();
	(void) fflush(stdout);
	(void) fflush(stderr);

	pid = fork();
	if (pid == 0)				/* child? */
		runctlmsg(ctlcmd, inname);
	else if (pid == -1)
		warning("fork failed", "");

	/* lint complains about &wstatus on 4.2+BSD; too bad, lint's wrong. */
	while ((deadpid = wait(&wstatus)) != pid && deadpid != -1)
		;

	/* wrong kid returned, fork failed or child screwed up? */
	if (deadpid == -1 || pid == -1 || wstatus != 0)
		art->a_status |= ST_DROPPED;	/* admin got err.msg. by mail */
	/* let lazy evaluation load the caches */
#else /* AMIGA */
	art->a_status |= synccaches();
	(void) fflush(stdout);
	(void) fflush(stderr);

	if (runctlmsg(ctlcmd, inname))
		art->a_status |= ST_DROPPED;	/* admin got err.msg. by mail */
#endif /* !AMIGA */
}

STATIC boolean
safecmd(cmd)			/* true if it's safe to system(3) cmd */
register char *cmd;
{
	register char *s;

	for (s = cmd; *s != '\0'; s++)
		if (STREQN(s, "..", STRLEN("..")))
			return NO;
	for (s = SHELLMETAS; *s != '\0'; s++)
		if (strchr(cmd, *s) != NULL)
			return NO;
	return YES;
}

/*
 * In theory (RFC 1036 nee 850), we should verify that the user issuing
 * the cancel (the Sender: of this article or From: if no Sender) is the
 * Sender: or From: of the original article or the local super-user.
 *
 * In practice, this is a lot of work and since anyone can forge news
 * (and thus cancel anything), not worth the effort.
 *
 * Ignore ST_ACCESS while cancelling an already-seen article since the
 * article may have been cancelled before or may have a fake history entry
 * because the cancel arrived before the article.
 *
 * If the article being cancelled has not been seen yet, generate a history
 * file entry for the cancelled article in case it arrives after the cancel
 * control.  The history file entry will cause the cancelled article to be
 * rejected as a duplicate.
 */
STATIC statust
cancelart(msgidstr)
char *msgidstr;
{
	register char *wsp;
	register char *msgid = strsave(msgidstr);
	register int idbytes;
	register char *wholemsgid = msgid;
	register statust status = ST_OKAY;

	/* skip leading whitespace in msgid */
	while (*msgid != '\0' && isascii(*msgid) && isspace(*msgid))
		++msgid;
	/* replace trailing whitespace with NULs; `wsp >= msgid' is not safe */
	idbytes = strlen(msgid);
	for (wsp = msgid + idbytes - 1; idbytes-- > 0 &&
	    isascii(*wsp) && isspace(*wsp); --wsp)
		*wsp = '\0';

	printf("Attempting to cancel article %s\n", msgid);
	if (alreadyseen(msgid)) {
		register char *histent, *filelist;

		histent = gethistory(msgid);
		if (histent != NULL && (filelist = findfiles(histent)) != NULL) {
			status |= snufffiles(filelist) & ~ST_ACCESS;
			fprintf(stderr,
				"  removed files \"%s\"; status 0x%04X\n", filelist, status);
		}
	} else {
		status |= fakehist(msgid, DEFEXP, NO_FILES);	/* start log */
		(void) putchar('\n');		/* end log line */
	}
	free(wholemsgid);
	return status;
}

/*
 * Execute a non-builtin control message by searching $NEWSCTL/bin and
 * $NEWSBIN/ctl for the command named by the control message.
 * runctlmsg is called from a child of relaynews, so it must always
 * call _exit() rather than exit() to avoid flushing stdio buffers.
 *
 * Enforce at least minimal security: the environment was standardised at
 * startup, including PATH and IFS; close non-standard file descriptors;
 * reject shell metacharacters in ctlcmd.
 */

#ifdef AMIGA
execute(register char *cmd, char *ctlcmd, char *inname)
{
	register int last;
	register char *cmdline;

	if (last = strcspn(ctlcmd, " \t\n")) {
		strncpy(cmd, ctlcmd, last);
		cmd[last] = '\0';
	} else {
		last = strlen(ctlcmd);
		strcpy(cmd, ctlcmd);
	}
	if ((cmdline = envparm(cmd)) == NULL)
		sprintf(cmd+last, "<%s %s", inname, ctlcmd+last);
	else
		sprintf(cmd, cmdline, inname, ctlcmd+last);
	return( !Execute(cmd, 0L, 0L) );
}
#endif /* AMIGA */


#ifdef AMIGA
	STATIC int runctlmsg(ctlcmd, inname)
#else
	STATIC void runctlmsg(ctlcmd, inname)			/* child process */
#endif
register char *ctlcmd, *inname;
{
	register char *cmd;
	register int cmdstat;

#ifdef AMIGA
# define EXIT	return
#else
# define EXIT	_exit
#endif

#ifndef AMIGA
	closeall(1);
	if (!safecmd(ctlcmd)) {
		(void) fprintf(stderr,
		    "%s: control `%s' looks unsafe to execute\n", progname, ctlcmd);
		(void) fflush(stderr);
		_exit(1);
	}
	cmd = malloc((unsigned) strlen("PATH=") + strlen(ctlfile("bin")) +
		strlen(":") + strlen(SUBDIR) + strlen(";") + strlen(ctlcmd) +
		strlen(" <") + strlen(inname) + SIZENUL);
	if (cmd == NULL) {
		warning("can't allocate memory in runctlmsg", "");
		(void) fflush(stderr);
		_exit(1);
	}
	(void) strcpy(cmd, "PATH=");
	(void) strcat(cmd, ctlfile("bin"));
	(void) strcat(cmd, ":");
	(void) strcat(cmd, SUBDIR);
	(void) strcat(cmd, ";");
	(void) strcat(cmd, ctlcmd);
	(void) strcat(cmd, " <");
	(void) strcat(cmd, inname);

	cmdstat = system(cmd);
	if (cmdstat != 0)
		bombctlmsg(cmd, cmdstat);
#else
	if (!safecmd(ctlcmd)) {
		(void) fprintf(stderr,
		    "%s: control `%s' looks unsafe to execute\n", progname, ctlcmd);
		EXIT(1);
	}
	cmd = malloc(strlen(ctlcmd) + STRLEN(" <") + strlen(inname) + 4);
	if (cmd == NULL) {
		warning("can't allocate memory in runctlmsg", "");
		EXIT(1);
	}
	cmdstat = execute(cmd, ctlcmd, inname);
	if (cmdstat != 0) {
		char *msg = malloc(strlen(progname) + strlen(cmd) + 60);
		sprintf(msg, "%s \"%s: control message `%s' unsuccessful\" %s",
			envparm("SENDMAIL"), progname, cmd, newsmaster());
		warning("control message `%s' unsuccessful", cmd);
		(void) Execute(msg, 0L, 0L);
		free(msg);
		free(cmd);
		EXIT(1);
	}
#endif /* AMIGA */
	free(cmd);
	EXIT(0);
}

#ifndef AMIGA
/*
 * Notify the local news administrator by mail that "cmd" failed
 * with "cmdstat" status, and _exit with bad status (again avoid stdio
 * buffer flushing in the child).
 */
STATIC void
bombctlmsg(cmd, cmdstat)
char *cmd;
int cmdstat;
{
	register char *mailcmd;
	register FILE *mailf;
	
	mailcmd = malloc((unsigned)strlen("PATH=") + strlen(newspath()) +
		strlen(" mail ") + strlen(newsmaster()) + SIZENUL);
	if (mailcmd == NULL) {
		warning("can't allocate memory in bombctlmsg", "");
		(void) fflush(stderr);
		_exit(1);
	}
	(void) sprintf(mailcmd, "PATH=%s mail %s", newspath(), newsmaster());
	mailf = popen(mailcmd, "w");
	if (mailf == NULL)
		mailf = stderr;
	(void) fprintf(mailf,
		"%s: control message `%s' exited with status 0%o\n",
		progname, cmd, cmdstat);
	(void) fflush(mailf);
	if (mailf != stderr)
		(void) pclose(mailf);
	free(mailcmd);
	_exit(1);
}
#endif /* !AMIGA */

STATIC boolean
oldctl(hdrs)				/* true iff ngs match OLDCNTRL */
register struct headers *hdrs;
{
#ifdef SLOWCTLMATCH
	return ngmatch(OLDCNTRL, hdrs->h_ngs);
#else
	register int ngslen = strlen(hdrs->h_ngs);

	if (ngslen < STRLEN(SFXOLDCNTRL))	/* ngs too short */
		return NO;
	else					/* check for .ctl suffix */
		/*
		 * This is more general than RFC 850 specifies, but this
		 * generality seems harmless.  This doesn't work for e.g.
		 * x.y.ctl,z.q, which is a darn shame, but that's a violation
		 * of common sense.
		 */
		return STREQ(&hdrs->h_ngs[ngslen-STRLEN(SFXOLDCNTRL)],
			SFXOLDCNTRL);
#endif						/* SLOWCTLMATCH */
}

hackoldctl(hdrs)			/* Handle the all.all.ctl hack. */
register struct headers *hdrs;
{
	if (hdrs->h_ctlcmd == NULL && oldctl(hdrs))
		hdrs->h_ctlcmd = strsave(hdrs->h_subj);
}

char *
hackhybrid(line)
register char *line;
{
	static char stupersedes[] = "Supersedes:";
	static char alsocan[] =     "Also-Control: cancel ";

	if (CISTREQN(line, stupersedes, STRLEN(stupersedes)))
		return str3save(alsocan, "", &line[STRLEN(stupersedes)]);
	else
		return strsave(line);
}
