/*
 * atheme-services: A collection of minimalist IRC services
 * services.c: Routines commonly used by various services.
 *
 * Copyright (c) 2005-2007 Atheme Project (http://www.atheme.org)
 *
 * Permission to use, copy, modify, and/or distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,
 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
 * IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

#include "atheme.h"
#include "pmodule.h"

int authservice_loaded = 0;
int use_myuser_access = 0;
int use_svsignore = 0;
int use_privmsg = 0;
int use_account_private = 0;
int use_channel_private = 0;
int use_limitflags = 0;

#define MAX_BUF 256

/* ban wrapper for cmode, returns number of bans added (0 or 1) */
int ban(user_t *sender, channel_t *c, user_t *user)
{
	char mask[MAX_BUF];
	chanban_t *cb;

	if (!c)
		return 0;

	snprintf(mask, MAX_BUF, "*!*@%s", user->vhost);
	mask[MAX_BUF - 1] = '\0';

	cb = chanban_find(c, mask, 'b');

	if (cb != NULL)
		return 0;

	chanban_add(c, mask, 'b');

	modestack_mode_param(sender->nick, c, MTYPE_ADD, 'b', mask);
	modestack_flush_now();

	return 1;
}

/* returns number of modes removed -- jilles */
int remove_banlike(user_t *source, channel_t *chan, int type, user_t *target)
{
	int count = 0;
	mowgli_node_t *n, *tn;
	chanban_t *cb;

	if (type == 0)
		return 0;
	if (source == NULL || chan == NULL || target == NULL)
		return 0;

	for (n = next_matching_ban(chan, target, type, chan->bans.head); n != NULL; n = next_matching_ban(chan, target, type, tn))
	{
		tn = n->next;
		cb = n->data;

		modestack_mode_param(source->nick, chan, MTYPE_DEL, cb->type, cb->mask);
		chanban_delete(cb);
		count++;
	}

	modestack_flush_now();

	return count;
}

/* returns number of exceptions removed -- jilles */
int remove_ban_exceptions(user_t *source, channel_t *chan, user_t *target)
{
	return remove_banlike(source, chan, ircd->except_mchar, target);
}

void try_kick_real(user_t *source, channel_t *chan, user_t *target, const char *reason)
{
	return_if_fail(source != NULL);
	return_if_fail(chan != NULL);
	return_if_fail(target != NULL);
	return_if_fail(reason != NULL);

	if (chan->modes & ircd->oimmune_mode && is_ircop(target))
	{
		wallops("Not kicking oper %s!%s@%s from protected %s (%s: %s)",
				target->nick, target->user, target->vhost,
				chan->name, source ? source->nick : me.name,
				reason);
		notice(source->nick, chan->name,
				"Not kicking oper %s (%s)",
				target->nick, reason);
		return;
	}
	if (target->flags & config_options.immune_level)
	{
		wallops("Not kicking immune user %s!%s@%s from %s (%s: %s)",
				target->nick, target->user, target->vhost,
				chan->name, source ? source->nick : me.name,
				reason);
		notice(source->nick, chan->name,
				"Not kicking immune user %s (%s)",
				target->nick, reason);
		return;
	}
	kick(source, chan, target, reason);
}
void (*try_kick)(user_t *source, channel_t *chan, user_t *target, const char *reason) = try_kick_real;

/* sends a KILL message for a user and removes the user from the userlist
 * source should be a service user or NULL for a server kill
 */
void kill_user(user_t *source, user_t *victim, const char *fmt, ...)
{
	va_list ap;
	char buf[BUFSIZE];
	char qreason[512];

	return_if_fail(victim != NULL);

	va_start(ap, fmt);
	vsnprintf(buf, BUFSIZE, fmt, ap);
	va_end(ap);

	if (victim->flags & UF_ENFORCER)
		quit_sts(victim, buf);
	else if (victim->server == me.me)
	{
		slog(LG_INFO, "kill_user(): not killing service %s (source %s, comment %s)",
				victim->nick,
				source != NULL ? source->nick : me.name,
				buf);
		return;
	}
	else
		kill_id_sts(source, CLIENT_NAME(victim), buf);
	snprintf(qreason, sizeof qreason, "Killed (%s (%s))",
			source != NULL ? source->nick : me.name, buf);
	user_delete(victim, qreason);
}

void introduce_enforcer(const char *nick)
{
	user_t *u;

	/* TS 1 to win nick collisions */
	u = user_add(nick, "enforcer", me.name, NULL, NULL,
			ircd->uses_uid ? uid_get() : NULL,
			"Held for nickname owner", me.me, 1);
	return_if_fail(u != NULL);
	u->flags |= UF_INVIS | UF_ENFORCER;
	introduce_nick(u);
}

/* join a channel, creating it if necessary */
void join(char *chan, char *nick)
{
	channel_t *c;
	user_t *u;
	chanuser_t *cu;
	bool isnew = false;
	mychan_t *mc;
	metadata_t *md;
	time_t ts;

	u = user_find_named(nick);
	if (!u)
		return;
	c = channel_find(chan);
	if (c == NULL)
	{
		mc = mychan_find(chan);
		if (chansvs.changets && mc != NULL)
		{
			/* Use the previous TS if known, registration
			 * time otherwise, but never ever create a channel
			 * with TS 0 -- jilles */
			ts = mc->registered;
			md = metadata_find(mc, "private:channelts");
			if (md != NULL)
				ts = atol(md->value);
			if (ts == 0)
				ts = CURRTIME;
		}
		else
			ts = CURRTIME;
		c = channel_add(chan, ts, me.me);
		c->modes |= CMODE_NOEXT | CMODE_TOPIC;
		if (mc != NULL)
			check_modes(mc, false);
		isnew = true;
	}
	else if (chanuser_find(c, u))
	{
		slog(LG_DEBUG, "join(): i'm already in `%s'", c->name);
		return;
	}
	join_sts(c, u, isnew, channel_modes(c, true));
	cu = chanuser_add(c, CLIENT_NAME(u));
	cu->modes |= CSTATUS_OP;
	if (isnew)
	{
		hook_call_channel_add(c);
	}
}

/* part a channel */
void part(char *chan, char *nick)
{
	channel_t *c = channel_find(chan);
	user_t *u = user_find_named(nick);

	if (!u || !c)
		return;
	if (!chanuser_find(c, u))
		return;
	if (me.connected)
		part_sts(c, u);
	chanuser_delete(c, u);
}

void services_init(void)
{
	service_t *svs;
	mowgli_patricia_iteration_state_t state;

	MOWGLI_PATRICIA_FOREACH(svs, &state, services_name)
	{
		if (ircd->uses_uid && svs->me->uid == NULL)
			user_changeuid(svs->me, uid_get());
		else if (!ircd->uses_uid && svs->me->uid != NULL)
			user_changeuid(svs->me, NULL);
		if (!ircd->uses_uid)
			kill_id_sts(NULL, svs->nick, "Attempt to use service nick");
		introduce_nick(svs->me);
	}
}

void joinall(char *name)
{
	service_t *svs;
	mowgli_patricia_iteration_state_t state;

	if (name == NULL)
		return;

	MOWGLI_PATRICIA_FOREACH(svs, &state, services_name)
	{
		/* service must be online and not a botserv bot */
		if (svs->me == NULL)
			continue;
		join(name, svs->me->nick);
	}
}

void partall(char *name)
{
	mowgli_patricia_iteration_state_t state;
	service_t *svs;
	mychan_t *mc;

	if (name == NULL)
		return;
	mc = mychan_find(name);
	MOWGLI_PATRICIA_FOREACH(svs, &state, services_name)
	{
		if (svs == chansvs.me && mc != NULL && mc->flags & MC_GUARD)
			continue;
		if (svs->me == NULL)
			continue;
		/* Do not cache this channel_find(), the
		 * channel may disappear under our feet
		 * -- jilles */
		if (chanuser_find(channel_find(name), svs->me))
			part(name, svs->me->nick);
	}
}

/* reintroduce a service e.g. after it's been killed -- jilles */
void reintroduce_user(user_t *u)
{
	mowgli_node_t *n;
	channel_t *c;
	service_t *svs;

	svs = service_find_nick(u->nick);
	if (svs == NULL)
	{
		slog(LG_DEBUG, "tried to reintroduce_user non-service %s", u->nick);
		return;
	}
	/* Reintroduce with a new UID.  This avoids problems distinguishing
	 * commands targeted at the old and new user.
	 */
	if (u->uid != NULL)
	{
		user_changeuid(u, uid_get());
	}
	else
	{
		/* Ensure it is really gone before introducing the new one.
		 * This also helps with nick collisions.
		 * With UID this is not necessary as we will
		 * reintroduce with a new UID, and nick collisions
		 * are unambiguous.
		 */
		if (!ircd->uses_uid)
			kill_id_sts(NULL, u->nick, "Service nick");
	}
	introduce_nick(u);
	MOWGLI_ITER_FOREACH(n, u->channels.head)
	{
		c = ((chanuser_t *)n->data)->chan;
		if (MOWGLI_LIST_LENGTH(&c->members) > 1 || c->modes & ircd->perm_mode)
			join_sts(c, u, 0, channel_modes(c, true));
		else
		{
			/* channel will have been destroyed... */
			/* XXX resend the bans instead of destroying them? */
			chanban_clear(c);
			join_sts(c, u, 1, channel_modes(c, true));
			if (c->topic != NULL)
				topic_sts(c, chansvs.me->me, c->topic_setter, c->topicts, 0, c->topic);
		}
	}
}

void verbose(mychan_t *mychan, const char *fmt, ...)
{
	va_list ap;
	char buf[BUFSIZE];

	if (mychan->chan == NULL)
		return;

	va_start(ap, fmt);
	vsnprintf(buf, BUFSIZE, fmt, ap);
	va_end(ap);

	if ((MC_VERBOSE | MC_FORCEVERBOSE) & mychan->flags)
		notice(chansvs.nick, mychan->name, "%s", buf);
	else if (MC_VERBOSE_OPS & mychan->flags)
		wallchops(chansvs.me->me, mychan->chan, buf);
}

/* protocol wrapper for nickchange/nick burst */
void handle_nickchange(user_t *u)
{
	service_t *svs;

	return_if_fail(u != NULL);
	return_if_fail(!is_internal_client(u));

	svs = service_find("global");

	if (runflags & RF_LIVE && log_debug_enabled())
		notice(svs != NULL ? svs->me->nick : me.name, u->nick, "Services are presently running in debug mode, attached to a console. You should take extra caution when utilizing your services passwords.");

	if (readonly)
		notice(svs != NULL ? svs->me->nick : me.name, u->nick, "Services are presently running in readonly mode.  Any changes you make will not be saved.");

	hook_call_nick_check(u);
}

/* User u is bursted as being logged in to login (if not NULL) or as
 * being identified to their current nick (if login is NULL)
 * The timestamp is the time of registration of the account, if the ircd
 * stores this, or 0 if not known.
 * Update the administration or log them out on ircd
 * How to use this in protocol modules:
 * 1. if login info is bursted in a command that always occurs, call
 *    this if the user is logged in, before handle_nickchange()
 * 2. if it is bursted in a command that doesn't always occur, use
 *    netwide EOB as in the ratbox module; call this if the user is logged
 *    in; for all users, postpone handle_nickchange() until the user's
 *    server confirms EOB
 * -- jilles
 */
void handle_burstlogin(user_t *u, char *login, time_t ts)
{
	mynick_t *mn;
	myuser_t *mu;
	mowgli_node_t *n;

	if (login != NULL)
		/* don't allow alias nicks here -- jilles */
		mu = myuser_find(login);
	else
	{
		mn = mynick_find(u->nick);
		mu = mn != NULL ? mn->owner : NULL;
		login = mu != NULL ? entity(mu)->name : u->nick;
	}
	if (mu == NULL)
	{
		/* account dropped during split...
		 * if we have an authentication service, log them out */
		if (authservice_loaded || backend_loaded)
		{
			slog(LG_DEBUG, "handle_burstlogin(): got nonexistent login %s for user %s", login, u->nick);
			if (authservice_loaded)
			{
				notice(nicksvs.nick ? nicksvs.nick : me.name, u->nick, _("Account %s dropped, forcing logout"), login);
				ircd_on_logout(u, login);
			}
			return;
		}
		/* we're running without a persistent db, create it */
		mu = myuser_add(login, "*", "noemail", MU_CRYPTPASS);
		if (ts != 0)
			mu->registered = ts;
		metadata_add(mu, "fake", "1");
	}
	if (u->myuser != NULL)	/* already logged in, hmm */
		return;
	if (ts != 0 && ts != mu->registered)
	{
		/* wrong account creation time
		 * if we have an authentication service, log them out */
		slog(LG_INFO, "handle_burstlogin(): got stale login %s for user %s", login, u->nick);
		if (authservice_loaded)
		{
			notice(nicksvs.nick ? nicksvs.nick : me.name, u->nick, _("Login to account %s is stale, forcing logout"), login);
			ircd_on_logout(u, login);
		}
		return;
	}
	if (mu->flags & MU_NOBURSTLOGIN && authservice_loaded)
	{
		/* no splits for this account, this bursted login cannot
		 * be legit...
		 * if we have an authentication service, log them out */
		slog(LG_INFO, "handle_burstlogin(): got illegit login %s for user %s", login, u->nick);
		notice(nicksvs.nick ? nicksvs.nick : me.name, u->nick, _("Login to account %s seems invalid, forcing logout"), login);
		ircd_on_logout(u, login);
		return;
	}
	u->myuser = mu;
	u->flags &= ~UF_SOPER_PASS;
	n = mowgli_node_create();
	mowgli_node_add(u, n, &mu->logins);
	slog(LG_DEBUG, "handle_burstlogin(): automatically identified %s as %s", u->nick, login);
}

void handle_setlogin(sourceinfo_t *si, user_t *u, char *login, time_t ts)
{
	mynick_t *mn;
	myuser_t *mu;
	mowgli_node_t *n;

	if (login != NULL)
		/* don't allow alias nicks here -- jilles */
		mu = myuser_find(login);
	else
	{
		mn = mynick_find(u->nick);
		mu = mn != NULL ? mn->owner : NULL;
		login = mu != NULL ? entity(mu)->name : u->nick;
	}

	if (authservice_loaded)
	{
		wallops("Ignoring attempt from %s to set login name for %s to %s",
				get_oper_name(si), u->nick, login);
		return;
	}

	if (u->myuser != NULL)
	{
		n = mowgli_node_find(u, &u->myuser->logins);
		if (n != NULL)
		{
			mowgli_node_delete(n, &u->myuser->logins);
			mowgli_node_free(n);
		}
		u->myuser = NULL;
	}
	if (mu == NULL)
	{
		if (backend_loaded)
		{
			slog(LG_DEBUG, "handle_setlogin(): got nonexistent login %s for user %s", login, u->nick);
			return;
		}
		/* we're running without a persistent db, create it */
		mu = myuser_add(login, "*", "noemail", MU_CRYPTPASS);
		if (ts != 0)
			mu->registered = ts;
		metadata_add(mu, "fake", "1");
	}
	else if (ts != 0 && ts != mu->registered)
	{
		if (backend_loaded)
		{
			slog(LG_DEBUG, "handle_setlogin(): got unexpected registration time for login %s for user %s (%lu != %lu)",
					login, u->nick,
					(unsigned long)ts,
					(unsigned long)mu->registered);
			return;
		}
		if (MOWGLI_LIST_LENGTH(&mu->logins))
			slog(LG_INFO, "handle_setlogin(): account %s with changing registration time has logins", login);
		slog(LG_DEBUG, "handle_setlogin(): changing registration time for %s from %lu to %lu",
				entity(mu)->name, (unsigned long)mu->registered,
				(unsigned long)ts);
		mu->registered = ts;
	}
	u->myuser = mu;
	u->flags &= ~UF_SOPER_PASS;
	n = mowgli_node_create();
	mowgli_node_add(u, n, &mu->logins);
	slog(LG_DEBUG, "handle_setlogin(): %s set %s logged in as %s",
			get_oper_name(si), u->nick, login);
}

void handle_clearlogin(sourceinfo_t *si, user_t *u)
{
	mowgli_node_t *n;

	if (authservice_loaded)
	{
		wallops("Ignoring attempt from %s to clear login name for %s",
				get_oper_name(si), u->nick);
		return;
	}

	if (u->myuser == NULL)
		return;

	slog(LG_DEBUG, "handle_clearlogin(): %s cleared login for %s (%s)",
			get_oper_name(si), u->nick, entity(u->myuser)->name);
	n = mowgli_node_find(u, &u->myuser->logins);
	if (n != NULL)
	{
		mowgli_node_delete(n, &u->myuser->logins);
		mowgli_node_free(n);
	}
	u->myuser = NULL;
}

void handle_certfp(sourceinfo_t *si, user_t *u, const char *certfp)
{
	myuser_t *mu;
	mycertfp_t *mcfp;
	service_t *svs;

	free(u->certfp);
	u->certfp = sstrdup(certfp);

	if (u->myuser != NULL)
		return;

	if ((mcfp = mycertfp_find(certfp)) == NULL)
		return;

	mu = mcfp->mu;
	svs = service_find("nickserv");
	if (svs == NULL)
		return;

	if (metadata_find(mu, "private:freeze:freezer"))
	{
		notice(svs->me->nick, u->nick, nicksvs.no_nick_ownership ? "You cannot login as \2%s\2 because the account has been frozen." : "You cannot identify to \2%s\2 because the nickname has been frozen.", entity(mu)->name);
		logcommand_user(svs, u, CMDLOG_LOGIN, "failed LOGIN to %s (frozen) via CERTFP (%s)", entity(mu)->name, certfp);
		return;
	}

	if (MOWGLI_LIST_LENGTH(&mu->logins) >= me.maxlogins)
	{
		notice(svs->me->nick, u->nick, _("There are already \2%zu\2 sessions logged in to \2%s\2 (maximum allowed: %u)."), MOWGLI_LIST_LENGTH(&mu->logins), entity(mu)->name, me.maxlogins);
		return;
	}

	notice(svs->me->nick, u->nick, nicksvs.no_nick_ownership ? _("You are now logged in as \2%s\2.") : _("You are now identified for \2%s\2."), entity(mu)->name);

	myuser_login(svs, u, mu, true);
	logcommand_user(svs, u, CMDLOG_LOGIN, "LOGIN via CERTFP (%s)", certfp);
}

void myuser_login(service_t *svs, user_t *u, myuser_t *mu, bool sendaccount)
{
	char lau[BUFSIZE], lao[BUFSIZE];
	char strfbuf[BUFSIZE];
	metadata_t *md_failnum;
	struct tm tm;
	mynick_t *mn;

	return_if_fail(svs != NULL && svs->me != NULL);
	return_if_fail(u->myuser == NULL);

	if (is_soper(mu))
		slog(LG_INFO, "SOPER: \2%s\2 as \2%s\2", u->nick, entity(mu)->name);

	myuser_notice(svs->me->nick, mu, "%s!%s@%s has just authenticated as you (%s)", u->nick, u->user, u->vhost, entity(mu)->name);

	u->myuser = mu;
	mowgli_node_add(u, mowgli_node_create(), &mu->logins);
	u->flags &= ~UF_SOPER_PASS;

	/* keep track of login address for users */
	mowgli_strlcpy(lau, u->user, BUFSIZE);
	mowgli_strlcat(lau, "@", BUFSIZE);
	mowgli_strlcat(lau, u->vhost, BUFSIZE);
	metadata_add(mu, "private:host:vhost", lau);

	/* and for opers */
	mowgli_strlcpy(lao, u->user, BUFSIZE);
	mowgli_strlcat(lao, "@", BUFSIZE);
	mowgli_strlcat(lao, u->host, BUFSIZE);
	metadata_add(mu, "private:host:actual", lao);

	/* check for failed attempts and let them know */
	if ((md_failnum = metadata_find(mu, "private:loginfail:failnum")) && (atoi(md_failnum->value) > 0))
	{
		metadata_t *md_failtime, *md_failaddr;
		time_t ts;

		notice(svs->me->nick, u->nick, "\2%d\2 failed %s since last login.",
			atoi(md_failnum->value), (atoi(md_failnum->value) == 1) ? "login" : "logins");

		md_failtime = metadata_find(mu, "private:loginfail:lastfailtime");
		ts = atol(md_failtime->value);
		md_failaddr = metadata_find(mu, "private:loginfail:lastfailaddr");

		tm = *localtime(&ts);
		strftime(strfbuf, sizeof strfbuf, TIME_FORMAT, &tm);

		notice(svs->me->nick, u->nick, "Last failed attempt from: \2%s\2 on %s.",
			md_failaddr->value, strfbuf);

		metadata_delete(mu, "private:loginfail:failnum");	/* md_failnum now invalid */
		metadata_delete(mu, "private:loginfail:lastfailtime");
		metadata_delete(mu, "private:loginfail:lastfailaddr");
	}

	mu->lastlogin = CURRTIME;
	mn = mynick_find(u->nick);
	if (mn != NULL && mn->owner == mu)
		mn->lastseen = CURRTIME;

	/* XXX: ircd_on_login supports hostmasking, we just dont have it yet. */
	/* don't allow them to join regonly chans until their
	 * email is verified */
	if (sendaccount && !(mu->flags & MU_WAITAUTH))
		ircd_on_login(u, mu, NULL);

	hook_call_user_identify(u);
}

/* this could be done with more finesse, but hey! */
void notice(const char *from, const char *to, const char *fmt, ...)
{
	va_list args;
	char buf[BUFSIZE];
	user_t *u;
	channel_t *c;

	va_start(args, fmt);
	vsnprintf(buf, BUFSIZE, fmt, args);
	va_end(args);

	if (*to == '#')
	{
		c = channel_find(to);
		if (c != NULL)
			notice_channel_sts(user_find_named(from), c, buf);
	}
	else
	{
		u = user_find_named(to);
		if (u != NULL)
		{
			if (u->myuser != NULL && u->myuser->flags & MU_USE_PRIVMSG)
				msg(from, to, "%s", buf);
			else
				notice_user_sts(user_find_named(from), u, buf);
		}
	}
}

/*
 * change_notify()
 *
 * Sends a change notification to a user affected by that change, provided
 * that he has not disabled the messages (MU_QUIETCHG is not set).
 *
 * Inputs:
 *       - string representing source (for compatibility with notice())
 *       - user_t object to send the notice to
 *       - printf-style string containing the data to send and any args
 *
 * Outputs:
 *       - nothing
 *
 * Side Effects:
 *       - a notice is sent to a user if MU_QUIETCHG is not set.
 */
void change_notify(const char *from, user_t *to, const char *fmt, ...)
{
	va_list args;
	char buf[BUFSIZE];

	va_start(args, fmt);
	vsnprintf(buf, BUFSIZE, fmt, args);
	va_end(args);

	if (is_internal_client(to))
		return;
	if (to->myuser != NULL && to->myuser->flags & MU_QUIETCHG)
		return;

	notice_user_sts(user_find_named(from), to, buf);
}

/*
 * bad_password()
 *
 * Registers an attempt to authenticate with an incorrect password.
 *
 * Inputs:
 *       - sourceinfo_t representing what sent the bad password
 *       - myuser_t object attempt was against
 *
 * Outputs:
 *       - whether the user was killed off the network
 *
 * Side Effects:
 *       - flood counter is incremented for user
 *       - attempt is registered in metadata
 *       - opers warned if necessary
 *
 * Note:
 *       - kills are currently not done
 */
bool bad_password(sourceinfo_t *si, myuser_t *mu)
{
	const char *mask;
	struct tm tm;
	char numeric[21], strfbuf[BUFSIZE];
	int count;
	metadata_t *md_failnum;
	service_t *svs;

	/* If the user is already logged in, no paranoia is needed,
	 * as they could /ns set password anyway.
	 */
	if (si->smu == mu)
		return false;

	command_add_flood(si, FLOOD_MODERATE);

	mask = get_source_mask(si);

	md_failnum = metadata_find(mu, "private:loginfail:failnum");
	count = md_failnum ? atoi(md_failnum->value) : 0;
	count++;
	snprintf(numeric, sizeof numeric, "%d", count);
	md_failnum = metadata_add(mu, "private:loginfail:failnum", numeric);
	metadata_add(mu, "private:loginfail:lastfailaddr", mask);
	snprintf(numeric, sizeof numeric, "%lu", (unsigned long)CURRTIME);
	metadata_add(mu, "private:loginfail:lastfailtime", numeric);

	svs = si->service;
	if (svs == NULL)
		svs = service_find("nickserv");
	if (svs != NULL)
	{
		myuser_notice(svs->me->nick, mu, "\2%s\2 failed to login to \2%s\2.  There has been \2%d\2 failed login %s since your last successful login.",
			      mask, entity(mu)->name, count, count == 1 ? "attempt" : "attempts");
	}

	if (is_soper(mu))
		slog(LG_INFO, "SOPER:AF: \2%s\2 as \2%s\2", get_source_name(si), entity(mu)->name);

	if (count == 10)
	{
		time_t ts = CURRTIME;
		tm = *localtime(&ts);
		strftime(strfbuf, sizeof strfbuf, TIME_FORMAT, &tm);
		wallops("Warning: Numerous failed login attempts to \2%s\2. Last attempt received from \2%s\2 on %s.", entity(mu)->name, mask, strfbuf);
	}

	return false;
}

mowgli_heap_t *sourceinfo_heap = NULL;

static void sourceinfo_delete(sourceinfo_t *si)
{
	mowgli_heap_free(sourceinfo_heap, si);
}

sourceinfo_t *sourceinfo_create(void)
{
	sourceinfo_t *out;

	if (sourceinfo_heap == NULL)
		sourceinfo_heap = sharedheap_get(sizeof(sourceinfo_t));

	out = mowgli_heap_alloc(sourceinfo_heap);
	object_init(object(out), "<sourceinfo>", (destructor_t) sourceinfo_delete);

	return out;
}

void command_fail(sourceinfo_t *si, faultcode_t code, const char *fmt, ...)
{
	va_list args;
	char buf[BUFSIZE];

	va_start(args, fmt);
	vsnprintf(buf, sizeof buf, fmt, args);
	va_end(args);

	if (si->v != NULL && si->v->cmd_fail)
	{
		si->v->cmd_fail(si, code, buf);
		return;
	}
	if (si->su == NULL)
		return;

	if (use_privmsg && si->smu != NULL && si->smu->flags & MU_USE_PRIVMSG)
		msg(si->service->nick, si->su->nick, "%s", buf);
	else
		notice_user_sts(si->service->me, si->su, buf);
}

void command_success_nodata(sourceinfo_t *si, const char *fmt, ...)
{
	va_list args;
	char buf[BUFSIZE];
	char *p, *q;
	char space[] = " ";

	if (si->output_limit && si->output_count > si->output_limit)
		return;
	si->output_count++;

	va_start(args, fmt);
	vsnprintf(buf, BUFSIZE, fmt, args);
	va_end(args);

	if (si->v != NULL && si->v->cmd_fail)
	{
		si->v->cmd_success_nodata(si, buf);
		return;
	}
	if (si->su == NULL)
		return;

	if (si->output_limit && si->output_count > si->output_limit)
	{
		notice(si->service->nick, si->su->nick, _("Output limit (%u) exceeded, halting output"), si->output_limit);
		return;
	}

	p = buf;
	do
	{
		q = strchr(p, '\n');
		if (q != NULL)
		{
			*q++ = '\0';
			if (*q == '\0')
				return; /* ending with \n */
		}
		if (*p == '\0')
			p = space; /* replace empty lines with a space */
		if (use_privmsg && si->smu != NULL && si->smu->flags & MU_USE_PRIVMSG)
			msg(si->service->nick, si->su->nick, "%s", p);
		else
			notice_user_sts(si->service->me, si->su, p);
		p = q;
	} while (p != NULL);
}

void command_success_string(sourceinfo_t *si, const char *result, const char *fmt, ...)
{
	va_list args;
	char buf[BUFSIZE];

	va_start(args, fmt);
	vsnprintf(buf, BUFSIZE, fmt, args);
	va_end(args);

	if (si->v != NULL && si->v->cmd_fail)
	{
		si->v->cmd_success_string(si, result, buf);
		return;
	}
	if (si->su == NULL)
		return;

	if (use_privmsg && si->smu != NULL && si->smu->flags & MU_USE_PRIVMSG)
		msg(si->service->nick, si->su->nick, "%s", buf);
	else
		notice_user_sts(si->service->me, si->su, buf);
}

static void command_table_cb(const char *line, void *data)
{
	command_success_nodata(data, "%s", line);
}

void command_success_table(sourceinfo_t *si, table_t *table)
{
	table_render(table, command_table_cb, si);
}

const char *get_source_name(sourceinfo_t *si)
{
	static char result[NICKLEN+NICKLEN+10];

	if (si->v != NULL && si->v->get_source_name != NULL)
		return si->v->get_source_name(si);

	if (si->su != NULL)
	{
		if (si->smu && !irccasecmp(si->su->nick, entity(si->smu)->name))
			snprintf(result, sizeof result, "%s", si->su->nick);
		else
			snprintf(result, sizeof result, "%s (%s)", si->su->nick,
					si->smu ? entity(si->smu)->name : "");
	}
	else if (si->s != NULL)
		snprintf(result, sizeof result, "%s", si->s->name);
	else
	{
		snprintf(result, sizeof result, "<%s>%s", si->v->description,
				si->smu ? entity(si->smu)->name : "");
	}
	return result;
}

const char *get_source_mask(sourceinfo_t *si)
{
	static char result[NICKLEN+USERLEN+HOSTLEN+10];

	if (si->v != NULL && si->v->get_source_mask != NULL)
		return si->v->get_source_mask(si);

	if (si->su != NULL)
	{
		snprintf(result, sizeof result, "%s!%s@%s", si->su->nick,
				si->su->user, si->su->vhost);
	}
	else if (si->s != NULL)
		snprintf(result, sizeof result, "%s", si->s->name);
	else
	{
		snprintf(result, sizeof result, "<%s>%s", si->v->description,
				si->smu ? entity(si->smu)->name : "");
	}
	return result;
}

const char *get_oper_name(sourceinfo_t *si)
{
	static char result[NICKLEN+USERLEN+HOSTLEN+NICKLEN+10];

	if (si->v != NULL && si->v->get_oper_name != NULL)
		return si->v->get_oper_name(si);

	if (si->su != NULL)
	{
		if (si->smu == NULL)
			snprintf(result, sizeof result, "%s!%s@%s{%s}", si->su->nick,
					si->su->user, si->su->vhost,
					si->su->server->name);
		else if (!irccasecmp(si->su->nick, entity(si->smu)->name))
			snprintf(result, sizeof result, "%s", si->su->nick);
		else
			snprintf(result, sizeof result, "%s (%s)", si->su->nick,
					si->smu ? entity(si->smu)->name : "");
	}
	else if (si->s != NULL)
		snprintf(result, sizeof result, "%s", si->s->name);
	else
	{
		snprintf(result, sizeof result, "<%s>%s", si->v->description,
				si->smu ? entity(si->smu)->name : "");
	}
	return result;
}

const char *get_storage_oper_name(sourceinfo_t *si)
{
	static char result[NICKLEN+USERLEN+HOSTLEN+NICKLEN+10];

	if (si->v != NULL && si->v->get_storage_oper_name != NULL)
		return si->v->get_storage_oper_name(si);

	if (si->smu != NULL)
		snprintf(result, sizeof result, "%s", entity(si->smu)->name);
	else if (si->su != NULL)
	{
		snprintf(result, sizeof result, "%s!%s@%s{%s}", si->su->nick,
				si->su->user, si->su->vhost,
				si->su->server->name);
	}
	else if (si->s != NULL)
		snprintf(result, sizeof result, "%s", si->s->name);
	else
		snprintf(result, sizeof result, "<%s>", si->v->description);
	return result;
}

void wallops(const char *fmt, ...)
{
	va_list args;
	char buf[BUFSIZE];

	if (config_options.silent)
		return;

	va_start(args, fmt);
	vsnprintf(buf, BUFSIZE, fmt, args);
	va_end(args);

	if (me.me != NULL && me.connected)
		wallops_sts(buf);
	else
		slog(LG_ERROR, "wallops(): unable to send: %s", buf);
}

void verbose_wallops(const char *fmt, ...)
{
	va_list args;
	char buf[BUFSIZE];

	if (config_options.silent || !config_options.verbose_wallops)
		return;

	va_start(args, fmt);
	vsnprintf(buf, BUFSIZE, fmt, args);
	va_end(args);

	if (me.me != NULL && me.connected)
		wallops_sts(buf);
	else
		slog(LG_ERROR, "verbose_wallops(): unable to send: %s", buf);
}

/* Check validity of a vhost against both general and protocol-specific rules.
 * Returns true if it is ok, false if not; command_fail() will have been called
 * as well.
 */
bool check_vhost_validity(sourceinfo_t *si, const char *host)
{
	const char *p;

	/* Never ever allow @!?* as they have special meaning in all ircds */
	/* Empty, space anywhere and colon at the start break the protocol */
	/* Also disallow ASCII 1-31 and "' as no sane IRCd allows them in hosts */
	if (strchr(host, '@') || strchr(host, '!') || strchr(host, '?') ||
			strchr(host, '*') || strchr(host, ' ') || strchr(host, '\'') ||
			strchr(host, '"') || *host == ':' || *host == '\0' ||
			has_ctrl_chars(host))
	{
		command_fail(si, fault_badparams, _("The vhost provided contains invalid characters."));
		return false;
	}
	if (strlen(host) >= HOSTLEN)
	{
		command_fail(si, fault_badparams, _("The vhost provided is too long."));
		return false;
	}
	p = strrchr(host, '/');
	if (p != NULL && isdigit(p[1]))
	{
		command_fail(si, fault_badparams, _("The vhost provided looks like a CIDR mask."));
		return false;
	}
	if (!is_valid_host(host))
	{
		/* This can be stuff like missing dots too. */
		command_fail(si, fault_badparams, _("The vhost provided is invalid."));
		return false;
	}
	return true;
}

/* vim:cinoptions=>s,e0,n0,f0,{0,}0,^0,=s,ps,t0,c3,+s,(2s,us,)20,*30,gs,hs
 * vim:ts=8
 * vim:sw=8
 * vim:noexpandtab
 */
