/*
 * timedban - Timed bans that are automatically unset.
 * (C) Copyright 2009-2017 Bram Matthys (Syzop) and the UnrealIRCd team.
 * License: GPLv2 or later
 *
 * This module adds an extended ban ~time:time:mask
 * Where 'time' is the time in minutes after which the ban will be removed.
 * Where 'mask' is any banmask that is normally valid.
 *
 * Note that this extended ban is rather special in the sense that
 * it permits (crazy) triple-extbans to be set, such as:
 * +b ~time:1:~q:~a:Account
 * (=a temporary 1min ban to mute a user with services account Account)
 * +e ~time:1440:~m:moderated:*!*@host
 * (=user with *!*@host may speak through +m for the next 1440m / 24h)
 *
 * The triple-extbans / double-stacking requires special routines that
 * are based on parts of the core and special recursion checks.
 * If you are looking for inspiration of coding your own extended ban
 * then look at another extended ban * module as this module is not a
 * good starting point ;)
 */
   
#include "unrealircd.h"

/* Maximum time (in minutes) for a ban */
#define TIMEDBAN_MAX_TIME	9999

/* Maximum length of a ban */
#define MAX_LENGTH 128

/* Split timeout event in <this> amount of iterations */
#define TIMEDBAN_TIMER_ITERATION_SPLIT 4

/* Call timeout event every <this> seconds.
 * NOTE: until all channels are processed it takes
 *       TIMEDBAN_TIMER_ITERATION_SPLIT * TIMEDBAN_TIMER.
 */
#define TIMEDBAN_TIMER	2

/* We allow a ban to (potentially) expire slightly before the deadline.
 * For example with TIMEDBAN_TIMER_ITERATION_SPLIT=4 and TIMEDBAN_TIMER=2
 * a 1 minute ban would expire at 56-63 seconds, rather than 60-67 seconds.
 * This is usually preferred.
 */
#define TIMEDBAN_TIMER_DELTA ((TIMEDBAN_TIMER_ITERATION_SPLIT*TIMEDBAN_TIMER)/2)

ModuleHeader MOD_HEADER
  = {
	"extbans/timedban",
	"1.0",
	"ExtBan ~time: automatically removed timed bans",
	"UnrealIRCd Team",
	"unrealircd-6",
    };

/* Forward declarations */
const char *timedban_extban_conv_param(BanContext *b, Extban *extban);
int timedban_extban_is_ok(BanContext *b);
int timedban_is_banned(BanContext *b);
void add_send_mode_param(Channel *channel, Client *from, char what, char mode, char *param);
char *timedban_chanmsg(Client *, Client *, Channel *, char *, int);

EVENT(timedban_timeout);

MOD_TEST()
{
	return MOD_SUCCESS;
}

MOD_INIT()
{
	ExtbanInfo extban;

	MARK_AS_OFFICIAL_MODULE(modinfo);

	memset(&extban, 0, sizeof(ExtbanInfo));
	extban.letter = 't';
	extban.name = "time";
	extban.options |= EXTBOPT_ACTMODIFIER; /* not really, but ours shouldn't be stacked from group 1 */
	extban.options |= EXTBOPT_INVEX; /* also permit timed invite-only exceptions (+I) */
	extban.conv_param = timedban_extban_conv_param;
	extban.is_ok = timedban_extban_is_ok;
	extban.is_banned = timedban_is_banned;
	extban.is_banned_events = BANCHK_ALL;

	if (!ExtbanAdd(modinfo->handle, extban))
	{
		config_error("timedban: unable to register 't' extban type!!");
		return MOD_FAILED;
	}
                
	EventAdd(modinfo->handle, "timedban_timeout", timedban_timeout, NULL, TIMEDBAN_TIMER*1000, 0);

	return MOD_SUCCESS;
}

MOD_LOAD()
{
	return MOD_SUCCESS;
}

MOD_UNLOAD()
{
	return MOD_SUCCESS;
}

/** Generic helper for our conv_param extban function.
 * Mostly copied from clean_ban_mask()
 * FIXME: Figure out why we have this one at all and not use conv_param? ;)
 */
const char *generic_clean_ban_mask(BanContext *b, Extban *extban)
{
	char *cp, *x;
	static char maskbuf[512];
	char *mask;

	/* Work on a copy */
	strlcpy(maskbuf, b->banstr, sizeof(maskbuf));
	mask = maskbuf;

	cp = strchr(mask, ' ');
	if (cp)
		*cp = '\0';

	/* Strip any ':' at beginning since that would cause a desync */
	for (; (*mask && (*mask == ':')); mask++);
	if (!*mask)
		return NULL;

	/* Forbid ASCII <= 32 in all bans */
	for (x = mask; *x; x++)
		if (*x <= ' ')
			return NULL;

	/* Extended ban? */
	if (is_extended_ban(mask))
	{
		const char *nextbanstr;
		Extban *extban = findmod_by_bantype(mask, &nextbanstr);
		if (!extban)
			return NULL; /* reject unknown extban */
		if (extban->conv_param)
		{
			const char *ret;
			static char retbuf[512];
			BanContext *newb = safe_alloc(sizeof(BanContext));
			newb->banstr = nextbanstr;
			newb->ban_type = b->ban_type;
			newb->conv_options = b->conv_options;
			newb->client = b->client;
			newb->channel = b->channel;
			ret = extban->conv_param(newb, extban);
			ret = prefix_with_extban(ret, newb, extban, retbuf, sizeof(retbuf));
			safe_free(newb);
			return ret;
		}
		/* else, do some basic sanity checks and cut it off at 80 bytes */
		if ((mask[1] != ':') || (mask[2] == '\0'))
		    return NULL; /* require a ":<char>" after extban type */
		if (strlen(mask) > 80)
			mask[80] = '\0';
		return mask;
	}

	return convert_regular_ban(mask, NULL, 0);
}

/** Convert ban to an acceptable format (or return NULL to fully reject it) */
const char *timedban_extban_conv_param(BanContext *b, Extban *extban)
{
	static char retbuf[MAX_LENGTH+1];
	char para[MAX_LENGTH+1];
	char tmpmask[MAX_LENGTH+1];
	char *durationstr; /**< Duration, such as '5' */
	int duration;
	char *matchby; /**< Matching method, such as 'n!u@h' */
	const char *newmask; /**< Cleaned matching method, such as 'n!u@h' */
	static int timedban_extban_conv_param_recursion = 0;
	
	if (timedban_extban_conv_param_recursion)
		return NULL; /* reject: recursion detected! */

	strlcpy(para, b->banstr, sizeof(para)); /* work on a copy (and truncate it) */
	
	/* ~time:duration:n!u@h   for direct matching
	 * ~time:duration:~x:.... when calling another bantype
	 */

	durationstr = para;
	matchby = strchr(para, ':');
	if (!matchby || !matchby[1])
		return NULL;
	*matchby++ = '\0';
	
	duration = atoi(durationstr);

	if ((duration <= 0) || (duration > TIMEDBAN_MAX_TIME))
		return NULL;

	strlcpy(tmpmask, matchby, sizeof(tmpmask));
	timedban_extban_conv_param_recursion++;
	//newmask = extban_conv_param_nuh_or_extban(tmpmask);
	b->banstr = matchby; // this was previously 'tmpmask' but then it's a copy-copy-copy.. :D
	newmask = generic_clean_ban_mask(b, extban);
	timedban_extban_conv_param_recursion--;
	if (!newmask || (strlen(newmask) <= 1))
		return NULL;

	//snprintf(retbuf, sizeof(retbuf), "~time:%d:%s", duration, newmask);
	snprintf(retbuf, sizeof(retbuf), "%d:%s", duration, newmask);
	return retbuf;
}

int timedban_extban_syntax(Client *client, int checkt, char *reason)
{
	if (MyUser(client) && (checkt == EXBCHK_PARAM))
	{
		sendnotice(client, "Error when setting timed ban: %s", reason);
		sendnotice(client, " Syntax: +b ~time:duration:mask");
		sendnotice(client, "Example: +b ~time:5:nick!user@host");
		sendnotice(client, "Duration is the time in minutes after which the ban is removed (1-9999)");
		sendnotice(client, "Valid masks are: nick!user@host or another extban type such as ~a, ~c, ~S, ..");
	}
	return 0; /* FAIL: ban rejected */
}

/** Generic helper for sub-bans, used by our "is this ban ok?" function */
int generic_ban_is_ok(BanContext *b)
{
	if ((b->banstr[0] == '~') && MyUser(b->client))
	{
		Extban *extban;
		const char *nextbanstr;

		/* This portion is copied from clean_ban_mask() */
		if (is_extended_ban(b->banstr) && MyUser(b->client))
		{
			/* And next is inspired by cmd_mode */
			extban = findmod_by_bantype(b->banstr, &nextbanstr);
			if (extban && extban->is_ok)
			{
				b->banstr = nextbanstr;
				if ((b->is_ok_check == EXBCHK_ACCESS) || (b->is_ok_check == EXBCHK_ACCESS_ERR))
				{
					if (!extban->is_ok(b) &&
					    !ValidatePermissionsForPath("channel:override:mode:extban",b->client,NULL,b->channel,NULL))
					{
						return 0; /* REJECT */
					}
				} else
				if (b->is_ok_check == EXBCHK_PARAM)
				{
					if (!extban->is_ok(b))
					{
						return 0; /* REJECT */
					}
				}
			}
		}
	}
	
	/* ACCEPT:
	 * - not an extban; OR
	 * - extban with NULL is_ok; OR
	 * - non-existing extban character (handled by conv_param?)
	 */
	return 1;
}

/** Validate ban ("is this ban ok?") */
int timedban_extban_is_ok(BanContext *b)
{
	char para[MAX_LENGTH+1];
	char tmpmask[MAX_LENGTH+1];
	char *durationstr; /**< Duration, such as '5' */
	int duration;
	char *matchby; /**< Matching method, such as 'n!u@h' */
	char *newmask; /**< Cleaned matching method, such as 'n!u@h' */
	static int timedban_extban_is_ok_recursion = 0;
	int res;

	/* Always permit deletion */
	if (b->what == MODE_DEL)
		return 1;

	if (timedban_extban_is_ok_recursion)
		return 0; /* Recursion detected (~time:1:~time:....) */

	strlcpy(para, b->banstr, sizeof(para)); /* work on a copy (and truncate it) */
	
	/* ~time:duration:n!u@h   for direct matching
	 * ~time:duration:~x:.... when calling another bantype
	 */

	durationstr = para;
	matchby = strchr(para, ':');
	if (!matchby || !matchby[1])
		return timedban_extban_syntax(b->client, b->is_ok_check, "Invalid syntax");
	*matchby++ = '\0';

	duration = atoi(durationstr);

	if ((duration <= 0) || (duration > TIMEDBAN_MAX_TIME))
		return timedban_extban_syntax(b->client, b->is_ok_check, "Invalid duration time");

	strlcpy(tmpmask, matchby, sizeof(tmpmask));
	timedban_extban_is_ok_recursion++;
	//res = extban_is_ok_nuh_extban(b->client, b->channel, tmpmask, b->is_ok_check, b->what, b->ban_type);
	b->banstr = tmpmask;
	res = generic_ban_is_ok(b);
	timedban_extban_is_ok_recursion--;
	if (res == 0)
	{
		/* This could be anything ranging from:
		 * invalid n!u@h syntax, unknown (sub)extbantype,
		 * disabled extban type in conf, too much recursion, etc.
		 */
		return timedban_extban_syntax(b->client, b->is_ok_check, "Invalid matcher");
	}

	return 1; /* OK */
}

/** Check if the user is currently banned */
int timedban_is_banned(BanContext *b)
{
	b->banstr = strchr(b->banstr, ':'); /* skip time argument */
	if (!b->banstr)
		return 0; /* invalid fmt */
	b->banstr++; /* skip over final semicolon */

	return ban_check_mask(b);
}

/** Helper to check if the ban has been expired.
 */
int timedban_has_ban_expired(Ban *ban)
{
	char *banstr = ban->banstr;
	char *p1, *p2;
	int t;
	time_t expire_on;

	/* The caller has only performed a very light check (string starting
	 * with ~time, in the interest of performance), so we don't know yet if
	 * it REALLY is a timed ban. We check that first here...
	 */
	if (!strncmp(banstr, "~t:", 3))
		p1 = banstr + 3;
	else if (!strncmp(banstr, "~time:", 6))
		p1 = banstr + 6;
	else
		return 0; /* not for us */
	p2 = strchr(p1+1, ':'); /* skip time argument */
	if (!p2)
		return 0; /* invalid fmt */
	*p2 = '\0'; /* danger.. must restore!! */
	t = atoi(p1);
	*p2 = ':'; /* restored.. */
	
	expire_on = ban->when + (t * 60) - TIMEDBAN_TIMER_DELTA;
	
	if (expire_on < TStime())
		return 1;
	return 0;
}

static char mbuf[512];
static char pbuf[512];

/** This removes any expired timedbans */
EVENT(timedban_timeout)
{
	Channel *channel;
	Ban *ban, *nextban;
	static int current_iteration = 0;

	if (++current_iteration >= TIMEDBAN_TIMER_ITERATION_SPLIT)
		current_iteration = 0;

	for (channel = channels; channel; channel = channel->nextch)
	{
		/* This is a very quick check, at the cost of it being
		 * biased since there's always a tendency of more channel
		 * names to start with one specific letter. But hashing
		 * is too costly. So we stick with this. It should be
		 * good enough. Alternative would be some channel->id value.
		 */
		if (((unsigned int)channel->name[1] % TIMEDBAN_TIMER_ITERATION_SPLIT) != current_iteration)
			continue; /* not this time, maybe next */

		*mbuf = *pbuf = '\0';
		for (ban = channel->banlist; ban; ban=nextban)
		{
			nextban = ban->next;
			if (!strncmp(ban->banstr, "~t", 2) && timedban_has_ban_expired(ban))
			{
				add_send_mode_param(channel, &me, '-',  'b', ban->banstr);
				del_listmode(&channel->banlist, channel, ban->banstr);
			}
		}
		for (ban = channel->exlist; ban; ban=nextban)
		{
			nextban = ban->next;
			if (!strncmp(ban->banstr, "~t", 2) && timedban_has_ban_expired(ban))
			{
				add_send_mode_param(channel, &me, '-',  'e', ban->banstr);
				del_listmode(&channel->exlist, channel, ban->banstr);
			}
		}
		for (ban = channel->invexlist; ban; ban=nextban)
		{
			nextban = ban->next;
			if (!strncmp(ban->banstr, "~t", 2) && timedban_has_ban_expired(ban))
			{
				add_send_mode_param(channel, &me, '-',  'I', ban->banstr);
				del_listmode(&channel->invexlist, channel, ban->banstr);
			}
		}
		if (*pbuf)
		{
			MessageTag *mtags = NULL;
			new_message(&me, NULL, &mtags);
			sendto_channel(channel, &me, NULL, 0, 0, SEND_LOCAL, mtags, ":%s MODE %s %s %s", me.name, channel->name, mbuf, pbuf);
			sendto_server(NULL, 0, 0, mtags, ":%s MODE %s %s %s 0", me.id, channel->name, mbuf, pbuf);
			free_message_tags(mtags);
			*pbuf = 0;
		}
	}
}

#if MODEBUFLEN > 512
 #error "add_send_mode_param() is not made for MODEBUFLEN > 512"
#endif

void add_send_mode_param(Channel *channel, Client *from, char what, char mode, char *param) {
	static char *modes = NULL, lastwhat;
	static short count = 0;
	short send = 0;
	
	if (!modes) modes = mbuf;
	
	if (!mbuf[0]) {
		modes = mbuf;
		*modes++ = what;
		*modes = 0;
		lastwhat = what;
		*pbuf = 0;
		count = 0;
	}
	if (lastwhat != what) {
		*modes++ = what;
		*modes = 0;
		lastwhat = what;
	}
	if (strlen(pbuf) + strlen(param) + 11 < MODEBUFLEN) {
		if (*pbuf) 
			strcat(pbuf, " ");
		strcat(pbuf, param);
		*modes++ = mode;
		*modes = 0;
		count++;
	}
	else if (*pbuf) 
		send = 1;

	if (count == MAXMODEPARAMS)
		send = 1;

	if (send)
	{
		MessageTag *mtags = NULL;

		new_message(&me, NULL, &mtags);
		sendto_channel(channel, &me, NULL, 0, 0, SEND_LOCAL, mtags, ":%s MODE %s %s %s", me.name, channel->name, mbuf, pbuf);
		sendto_server(NULL, 0, 0, mtags, ":%s MODE %s %s %s 0", me.id, channel->name, mbuf, pbuf);
		free_message_tags(mtags);
		send = 0;
		*pbuf = 0;
		modes = mbuf;
		*modes++ = what;
		lastwhat = what;
		if (count != MAXMODEPARAMS)
		{
			strlcpy(pbuf, param, sizeof(pbuf));
			*modes++ = mode;
			count = 1;
		} else {
			count = 0;
		}
		*modes = 0;
	}
}
