/*
 *   IRC - Internet Relay Chat, src/modules/chghost.c
 *   (C) 1999-2001 Carsten Munk (Techie/Stskeeps) <stskeeps@tspre.org>
 *
 *   See file AUTHORS in IRC package for additional names of
 *   the programmers. 
 *
 *   This program is free software; you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation; either version 1, or (at your option)
 *   any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program; if not, write to the Free Software
 *   Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

#include "unrealircd.h"

#define MSG_CHGHOST 	"CHGHOST"

CMD_FUNC(cmd_chghost);
void _userhost_save_current(Client *client);
void _userhost_changed(Client *client);

long CAP_CHGHOST = 0L;

ModuleHeader MOD_HEADER
  = {
	"chghost",	/* Name of module */
	"5.0", /* Version */
	"/chghost", /* Short description of module */
	"UnrealIRCd Team",
	"unrealircd-6",
    };

MOD_TEST()
{
	MARK_AS_OFFICIAL_MODULE(modinfo);
	EfunctionAddVoid(modinfo->handle, EFUNC_USERHOST_SAVE_CURRENT, _userhost_save_current);
	EfunctionAddVoid(modinfo->handle, EFUNC_USERHOST_CHANGED, _userhost_changed);
	return MOD_SUCCESS;
}

MOD_INIT()
{
	ClientCapabilityInfo c;

	CommandAdd(modinfo->handle, MSG_CHGHOST, cmd_chghost, MAXPARA, CMD_USER|CMD_SERVER);
	MARK_AS_OFFICIAL_MODULE(modinfo);

	memset(&c, 0, sizeof(c));
	c.name = "chghost";
	ClientCapabilityAdd(modinfo->handle, &c, &CAP_CHGHOST);

	return MOD_SUCCESS;
}

MOD_LOAD()
{
	return MOD_SUCCESS;
	
}

MOD_UNLOAD()
{
	return MOD_SUCCESS;	
}


static char remember_nick[NICKLEN+1];
static char remember_user[USERLEN+1];
static char remember_host[HOSTLEN+1];

/** Save current nick/user/host. Used later by userhost_changed(). */
void _userhost_save_current(Client *client)
{
	strlcpy(remember_nick, client->name, sizeof(remember_nick));
	strlcpy(remember_user, client->user->username, sizeof(remember_user));
	strlcpy(remember_host, GetHost(client), sizeof(remember_host));
}

/** User/Host changed for user.
 * Note that userhost_save_current() needs to be called before this
 * to save the old username/hostname.
 * This userhost_changed() function deals with notifying local clients
 * about the user/host change by sending PART+JOIN+MODE if
 * set::allow-userhost-change force-rejoin is in use,
 * and it wills end "CAP chghost" to such capable clients.
 * It will also deal with bumping fakelag for the user since a user/host
 * change is costly, doesn't matter if it was self-induced or not.
 *
 * Please call this function for any user/host change by doing:
 * userhost_save_current(client);
 * << change username or hostname here >>
 * userhost_changed(client);
 */
void _userhost_changed(Client *client)
{
	Membership *channels;
	Member *lp;
	Client *acptr;
	int impact = 0;
	char buf[512];
	long CAP_EXTENDED_JOIN = ClientCapabilityBit("extended-join");

	if (strcmp(remember_nick, client->name))
	{
		unreal_log(ULOG_ERROR, "main", "BUG_USERHOST_CHANGED", client,
		           "[BUG] userhost_changed() was called but without calling userhost_save_current() first! Affected user: $client\n"
		           "Please report above bug on https://bugs.unrealircd.org/");
		return; /* We cannot safely process this request anymore */
	}

	/* It's perfectly acceptable to call us even if the userhost didn't change. */
	if (!strcmp(remember_user, client->user->username) && !strcmp(remember_host, GetHost(client)))
		return; /* Nothing to do */

	/* Most of the work is only necessary for set::allow-userhost-change force-rejoin */
	if (UHOST_ALLOWED == UHALLOW_REJOIN)
	{
		/* Walk through all channels of this user.. */
		for (channels = client->user->channel; channels; channels = channels->next)
		{
			Channel *channel = channels->channel;
			char *modes;
			char partbuf[512]; /* PART */
			char joinbuf[512]; /* JOIN */
			char exjoinbuf[512]; /* JOIN (for CAP extended-join) */
			char modebuf[512]; /* MODE (if any) */
			int chanops_only = invisible_user_in_channel(client, channel);

			modebuf[0] = '\0';

			/* If the user is banned, don't send any rejoins, it would only be annoying */
			if (is_banned(client, channel, BANCHK_JOIN, NULL, NULL))
				continue;

			/* Prepare buffers for PART, JOIN, MODE */
			ircsnprintf(partbuf, sizeof(partbuf), ":%s!%s@%s PART %s :%s",
						remember_nick, remember_user, remember_host,
						channel->name,
						"Changing host");

			ircsnprintf(joinbuf, sizeof(joinbuf), ":%s!%s@%s JOIN %s",
						client->name, client->user->username, GetHost(client), channel->name);

			ircsnprintf(exjoinbuf, sizeof(exjoinbuf), ":%s!%s@%s JOIN %s %s :%s",
				client->name, client->user->username, GetHost(client), channel->name,
				IsLoggedIn(client) ? client->user->account : "*",
				client->info);

			modes = get_chmodes_for_user(client, channels->member_modes);
			if (!BadPtr(modes))
				ircsnprintf(modebuf, sizeof(modebuf), ":%s MODE %s %s", me.name, channel->name, modes);

			for (lp = channel->members; lp; lp = lp->next)
			{
				acptr = lp->client;

				if (acptr == client)
					continue; /* skip self */

				if (!MyConnect(acptr))
					continue; /* only locally connected clients */

				if (chanops_only && !check_channel_access_member(lp, "hoaq"))
					continue; /* skip non-ops if requested to (used for mode +D) */

				if (HasCapabilityFast(acptr, CAP_CHGHOST))
					continue; /* we notify 'CAP chghost' users in a different way, so don't send it here. */

				impact++;

				/* FIXME: if a client does not have the "chghost" cap then
				 * here we will not generate a proper new message, probably
				 * needs to be fixed... I skipped doing it for now.
				 */
				sendto_one(acptr, NULL, "%s", partbuf);

				if (HasCapabilityFast(acptr, CAP_EXTENDED_JOIN))
					sendto_one(acptr, NULL, "%s", exjoinbuf);
				else
					sendto_one(acptr, NULL, "%s", joinbuf);

				if (*modebuf)
					sendto_one(acptr, NULL, "%s", modebuf);
			}
		}
	}

	/* Now deal with "CAP chghost" clients.
	 * This only needs to be sent one per "common channel".
	 * This would normally call sendto_common_channels_local_butone() but the user already
	 * has the new user/host.. so we do it here..
	 */
	ircsnprintf(buf, sizeof(buf), ":%s!%s@%s CHGHOST %s %s",
	            remember_nick, remember_user, remember_host,
	            client->user->username,
	            GetHost(client));
	current_serial++;
	for (channels = client->user->channel; channels; channels = channels->next)
	{
		for (lp = channels->channel->members; lp; lp = lp->next)
		{
			acptr = lp->client;
			if (MyUser(acptr) && HasCapabilityFast(acptr, CAP_CHGHOST) &&
			    (acptr->local->serial != current_serial) && (client != acptr))
			{
				/* FIXME: send mtag */
				sendto_one(acptr, NULL, "%s", buf);
				acptr->local->serial = current_serial;
			}
		}
	}
	
	RunHook(HOOKTYPE_USERHOST_CHANGE, client, remember_user, remember_host);

	if (MyUser(client))
	{
		/* We take the liberty of sending the CHGHOST to the impacted user as
		 * well. This makes things easy for client coders.
		 * (Note that this cannot be merged with the for loop from 15 lines up
		 *  since the user may not be in any channels)
		 */
		if (HasCapabilityFast(client, CAP_CHGHOST))
			sendto_one(client, NULL, "%s", buf);

		if (MyUser(client))
			sendnumeric(client, RPL_HOSTHIDDEN, GetHost(client));

		/* A userhost change always generates the following network traffic:
		 * server to server traffic, CAP "chghost" notifications, and
		 * possibly PART+JOIN+MODE if force-rejoin had work to do.
		 * We give the user a penalty so they don't flood...
		 */
		if (impact)
			add_fake_lag(client, 7000); /* Resulted in rejoins and such. */
		else
			add_fake_lag(client, 4000); /* No rejoins */
	}
}

/* 
 * cmd_chghost - 12/07/1999 (two months after I made SETIDENT) - Stskeeps
 * :prefix CHGHOST <nick> <new hostname>
 * parv[1] - target user
 * parv[2] - hostname
 *
*/

CMD_FUNC(cmd_chghost)
{
	Client *target;

	if (MyUser(client) && !ValidatePermissionsForPath("client:set:host",client,NULL,NULL,NULL))
	{
		sendnumeric(client, ERR_NOPRIVILEGES);
		return;
	}

	if ((parc < 3) || BadPtr(parv[2]))
	{
		sendnumeric(client, ERR_NEEDMOREPARAMS, "CHGHOST");
		return;
	}

	if (strlen(parv[2]) > (HOSTLEN))
	{
		sendnotice(client, "*** ChgName Error: Requested hostname too long -- rejected.");
		return;
	}

	if (!valid_host(parv[2], 0))
	{
		sendnotice(client, "*** /ChgHost Error: A hostname may contain a-z, A-Z, 0-9, '-' & '.' - Please only use them");
		return;
	}

	if (parv[2][0] == ':')
	{
		sendnotice(client, "*** A hostname cannot start with ':'");
		return;
	}

	target = find_client(parv[1], NULL);
	if (!MyUser(client) && !target && (target = find_server_by_uid(parv[1])))
	{
		/* CHGHOST for a UID that is not online.
		 * Let's assume it may not YET be online and forward the message to
		 * the remote server and stop processing ourselves.
		 * That server will then handle pre-registered processing of the
		 * CHGHOST and later communicate the host when the user actually
		 * comes online in the UID message.
		 */
		sendto_one(target, recv_mtags, ":%s CHGHOST %s %s", client->id, parv[1], parv[2]);
		return;
	}

	if (!target || !target->user)
	{
		sendnumeric(client, ERR_NOSUCHNICK, parv[1]);
		return;
	}

	if (!strcmp(GetHost(target), parv[2]))
	{
		sendnotice(client, "*** /ChgHost Error: requested host is same as current host.");
		return;
	}

	userhost_save_current(target);

	switch (UHOST_ALLOWED)
	{
		case UHALLOW_NEVER:
			if (MyUser(client))
			{
				sendnumeric(client, ERR_DISABLED, "CHGHOST",
					"This command is disabled on this server");
				return;
			}
			break;
		case UHALLOW_ALWAYS:
			break;
		case UHALLOW_NOCHANS:
			if (IsUser(target) && MyUser(client) && target->user->joined)
			{
				sendnotice(client, "*** /ChgHost can not be used while %s is on a channel", target->name);
				return;
			}
			break;
		case UHALLOW_REJOIN:
			/* rejoin sent later when the host has been changed */
			break;
	}

	if (!IsULine(client))
	{
		const char *issuer = command_issued_by_rpc(recv_mtags);
		if (issuer)
		{
			unreal_log(ULOG_INFO, "chgcmds", "CHGHOST_COMMAND", client,
				   "CHGHOST: $issuer changed the virtual hostname of $target.details to be $new_hostname",
				   log_data_string("issuer", issuer),
				   log_data_string("change_type", "hostname"),
				   log_data_client("target", target),
				   log_data_string("new_hostname", parv[2]));
		} else {
			unreal_log(ULOG_INFO, "chgcmds", "CHGHOST_COMMAND", client,
				   "CHGHOST: $client changed the virtual hostname of $target.details to be $new_hostname",
				   log_data_string("change_type", "hostname"),
				   log_data_client("target", target),
				   log_data_string("new_hostname", parv[2]));
		}
	}

	target->umodes |= UMODE_HIDE;
	target->umodes |= UMODE_SETHOST;

	/* Send to other servers too, unless the client is still in the registration phase (SASL) */
	if (IsUser(target))
		sendto_server(client, 0, 0, recv_mtags, ":%s CHGHOST %s %s", client->id, target->id, parv[2]);

	safe_strdup(target->user->virthost, parv[2]);
	userhost_changed(target);
}
