/* S-line module.
 *
 * IRC Services is copyright (c) 1996-2002 Andrew Church.
 *     E-mail: <achurch@achurch.org>
 * Parts written by Andrew Kempe and others.
 * This program is free but copyrighted software; see the file COPYING for
 * details.
 */

#include "services.h"
#include "modules.h"
#include "conffile.h"
#include "commands.h"
#include "language.h"

#include "operserv.h"
#define NEED_MAKE_REASON
#include "maskdata.h"
#include "sline.h"

/*************************************************************************/

static Module *module;
static Module *module_operserv;
static Module *module_nickserv;

static int cb_send_sgline   = -1;
static int cb_send_sqline   = -1;
static int cb_send_szline   = -1;
static int cb_cancel_sgline = -1;
static int cb_cancel_sqline = -1;
static int cb_cancel_szline = -1;

static char * SlineDBName;
static char * SGlineReason;
static char * SQlineReason;
static char * SZlineReason;
static int    ImmediatelySendSline;
static time_t SGlineExpiry;
static time_t SQlineExpiry;
static time_t SZlineExpiry;
static int    WallOSSline;
static int    WallSlineExpire;
static int    SQlineIgnoreOpers;
static int    SQlineKill;

static int db_opened = 0;

/* Set nonzero if we don't have client IP info:
 *    -1 = no IP info and no SZLINE-like command either
 *    +1 = no IP info, but SZLINE command available
 */
static int no_szline = 0;

static void do_sgline(User *u);
static void do_sqline(User *u);
static void do_szline(User *u);

static uint8 sline_types[3] = {MD_SGLINE, MD_SQLINE, MD_SZLINE};

static Command cmds[] = {
    {"SGLINE",    do_sgline,    is_services_oper, OPER_HELP_SGLINE,    -1,-1},
    {"SQLINE",    do_sqline,    is_services_oper, OPER_HELP_SQLINE,    -1,-1},
    {"SZLINE",    do_szline,    is_services_oper, OPER_HELP_SZLINE,    -1,-1},
    { NULL }
};

/*************************************************************************/
/************************** Internal functions ***************************/
/*************************************************************************/

/* Callback for saving data. */

static int do_save_data(void)
{
    sync_sline_db(SlineDBName);
    return 0;
}

/*************************************************************************/

/* Send an S-line to the uplink server. */

static void send_sline(uint8 type, const MaskData *sline)
{
    int cb;
    const char *reason;

    if (type == MD_SGLINE) {
	cb = cb_send_sgline;
	reason = SGlineReason;
    } else if (type == MD_SQLINE && !SQlineKill) {
	cb = cb_send_sqline;
	reason = SQlineReason;
    } else if (type == MD_SZLINE) {
	cb = cb_send_szline;
	reason = SZlineReason;
    } else {
	return;
    }
    call_callback_4(module, cb, sline->mask, sline->expires, sline->who,
		    make_reason(reason, sline));
}

/*************************************************************************/

/* Remove an S-line from the uplink server. */

static void cancel_sline(uint8 type, char *mask)
{
    int cb;

    if (type == MD_SGLINE) {
	cb = cb_cancel_sgline;
    } else if (type == MD_SQLINE) {
	cb = cb_cancel_sqline;
    } else if (type == MD_SZLINE) {
	cb = cb_cancel_szline;
    } else {
	return;
    }
    call_callback_1(module, cb, mask);
}

/*************************************************************************/
/************************** Callback functions ***************************/
/*************************************************************************/

/* Does the user match any S-lines?  Return 1 (and kill the user) if so,
 * else 0.  Note that if a Q:line (and no G:line or Z:line) is matched, and
 * SQlineKill is not set, we send a 432 (erroneous nickname) reply to the
 * client and change their nick if the ircd supports forced nick changing.
 */

static int check_sline(int ac, char **av)
{
    const char *nick = av[0], *name = av[6], *ip = (ac>=9 ? av[8] : NULL);
    MaskData *sline;

    if (noakill)
	return 0;

    if (ip) {
	sline = get_matching_maskdata(MD_SZLINE, ip);
	if (sline) {
	    send_cmd(s_OperServ, "KILL %s :%s (%s)", nick, s_OperServ,
		     make_reason(SZlineReason, sline));
	    send_sline(MD_SZLINE, sline);
	    time(&sline->lastused);
	    put_maskdata(MD_SZLINE, sline);
	    return 1;
	}
    } else {
	if (!no_szline) {
	    if (protocol_features & PF_SZLINE) {
		if (!ImmediatelySendSline) {
		    wallops(s_OperServ,
			    "\2WARNING\2: Client IP addresses are not"
			    " available with this IRC server; SZLINEs"
			    " cannot be used unless ImmediatelySendSline"
			    " is enabled in %s.", MODULES_CONF);
		    no_szline = -1;
		} else {
		    no_szline = 1;
		}
	    } else {
		wallops(s_OperServ,
			"\2WARNING:\2 Client IP addresses are not available"
			" with this IRC server; SZLINEs cannot be used.");
		no_szline = -1;
	    }
	    module_log("warning: client IP addresses not available with"
		       " this IRC server");
	}
    }

    sline = get_matching_maskdata(MD_SGLINE, name);
    if (sline) {
	/* Don't use kill_user(); that's for people who have already signed
	 * on.  This is called before the User structure is created. */
	send_cmd(s_OperServ, "KILL %s :%s (%s)", nick, s_OperServ,
		 make_reason(SGlineReason, sline));
	send_sline(MD_SGLINE, sline);
	time(&sline->lastused);
	put_maskdata(MD_SGLINE, sline);
	return 1;
    }

    sline = get_matching_maskdata(MD_SQLINE, nick);
    if (sline) {
	int retval = 0;
	/* Don't kill/nickchange users if they just got changed to a guest
	 * nick */
	if (!is_guest_nick(nick)) {
	    char *reason = make_reason(SQlineReason, sline);
	    if (!SQlineKill && (protocol_features & PF_CHANGENICK)) {
		send_cmd(ServerName, "432 %s %s Invalid nickname (%s)",
			 nick, nick, reason);
		send_nickchange_remote(nick, make_guest_nick());
	    } else {
		send_cmd(s_OperServ, "KILL %s :%s (%s)", nick, s_OperServ,
			 reason);
		retval = 1;
	    }
	}
	send_sline(MD_SQLINE, sline);
	time(&sline->lastused);
	put_maskdata(MD_SQLINE, sline);
	return retval;
    }

    return 0;
}

/*************************************************************************/

/* Does the user (who has just changed their nick) match any SQlines?  If
 * so, send them a 432 and, on servers supporting forced nick changing,
 * change their nick (if !SQlineKill).
 */

static int check_sqline_nickchange(User *u, const char *oldnick)
{
    MaskData *sline;

    if (SQlineIgnoreOpers && is_oper(u))
	return 0;

    sline = get_matching_maskdata(MD_SQLINE, u->nick);
    if (sline) {
	int retval = 0;
	/* Don't kill/nickchange users if they just got changed to a guest
	 * nick */
	if (!is_guest_nick(u->nick)) {
	    char *reason = make_reason(SQlineReason, sline);
	    if (!SQlineKill && (protocol_features & PF_CHANGENICK)) {
		send_cmd(ServerName, "432 %s %s Invalid nickname (%s)",
			 u->nick, u->nick, reason);
		send_nickchange_remote(u->nick, make_guest_nick());
	    } else {
		kill_user(s_OperServ, u->nick, reason);
		retval = 1;
	    }
	}
	send_sline(MD_SQLINE, sline);
	time(&sline->lastused);
	put_maskdata(MD_SQLINE, sline);
	return retval;
    }

    return 0;
}

/*************************************************************************/

/* Callback for NickServ REGISTER/LINK check; we disallow
 * registration/linking of SQlined nicknames.
 */

static int do_reglink_check(const User *u, const char *nick,
			    const char *pass, const char *email)
{
    return get_matching_maskdata(MD_SQLINE, nick) != NULL;
}

/*************************************************************************/
/************************** S-line list editing **************************/
/*************************************************************************/

/* Note that all string parameters are assumed to be non-NULL; expiry must
 * be set to the time when the S-line should expire (0 for none).  Mask
 * is converted to lowercase on return.
 */

EXPORT_FUNC(create_sline)
void create_sline(uint8 type, char *mask, const char *reason,
		  const char *who, time_t expiry)
{
    MaskData *sline;

    strlower(mask);
    if (maskdata_count(type) >= 32767) {
	module_log("Attempt to add S%cLINE to full list!", type);
	return;
    }
    sline = scalloc(1, sizeof(*sline));
    sline->mask = sstrdup(mask);
    sline->reason = sstrdup(reason);
    sline->time = time(NULL);
    sline->expires = expiry;
    strscpy(sline->who, who, NICKMAX);
    sline = add_maskdata(type, sline);
    if (ImmediatelySendSline)
	send_sline(type, sline);
}

/*************************************************************************/

/* Handle an OperServ S*LINE command. */

static void do_sline(uint8 type, User *u);
static void do_sgline(User *u) { do_sline(MD_SGLINE, u); }
static void do_sqline(User *u) { do_sline(MD_SQLINE, u); }
static void do_szline(User *u) {
    if (no_szline < 0)
	notice_lang(s_OperServ, u, OPER_SZLINE_NOT_AVAIL);
    else
	do_sline(MD_SZLINE, u);
}

static void do_add_sline(User *u, uint8 type, MaskData *md);
static void do_del_sline(User *u, uint8 type, MaskData *md);

static MaskDataCmdInfo sline_cmd_info = {
    "SxLINE",		/* command name (overwritten later) */
    0,			/* MaskData type (overwritten later) */
    NULL,		/* default expiry time pointer (overwritten later) */

    OPER_SLINE_SYNTAX,
    OPER_SLINE_ADD_SYNTAX,
    OPER_SLINE_DEL_SYNTAX,
    OPER_TOO_MANY_SLINES,
    OPER_SLINE_EXISTS,
    OPER_SLINE_ADDED,
    OPER_SLINE_NOT_FOUND,
    OPER_SLINE_REMOVED,
    OPER_SLINE_LIST_HEADER,
    OPER_SLINE_LIST_FORMAT,
    OPER_SLINE_VIEW_FORMAT,
    OPER_SLINE_VIEW_UNUSED_FORMAT,
    OPER_SLINE_COUNT,

    NULL,		/* function to mangle masks for add/delete */
    NULL,		/* function to check validity of mask on add */
    do_add_sline,	/* function to call on mask addition */
    do_del_sline,	/* function to call on mask removal */
    NULL,		/* function to call for unknown commands */
};

static void do_sline(uint8 type, User *u)
{
    char sxline[7];

    sprintf(sxline, "S%cLINE", type);
    sline_cmd_info.name = sxline;
    sline_cmd_info.md_type = type;
    switch (type) {
      case MD_SGLINE:
	sline_cmd_info.def_expiry_ptr = &SGlineExpiry;
	break;
      case MD_SQLINE:
	sline_cmd_info.def_expiry_ptr = &SQlineExpiry;
	break;
      case MD_SZLINE:
	sline_cmd_info.def_expiry_ptr = &SZlineExpiry;
	break;
      default:
	module_log("do_sline(): bad type value (%u)", type);
	notice_lang(s_OperServ, u, INTERNAL_ERROR);
	return;
    }
    do_maskdata_cmd(&sline_cmd_info, u);
}

/*************************************************************************/

static void do_add_sline(User *u, uint8 type, MaskData *md)
{
    if (WallOSSline) {
	char buf[128];
	expires_in_lang(buf, sizeof(buf), NULL, md->expires);
	wallops(s_OperServ, "%s added an S%cLINE for \2%s\2 (%s)",
		u->nick, type, md->mask, buf);
    }
    if (ImmediatelySendSline)
	send_sline(type, md);
}

/*************************************************************************/

static void do_del_sline(User *u, uint8 type, MaskData *md)
{
    cancel_sline(type, md->mask);
}

/*************************************************************************/
/******************************* Callbacks *******************************/
/*************************************************************************/

/* Callback for S-line expiration. */

static int do_expire_maskdata(uint32 type, MaskData *md)
{
    int i;
    for (i = 0; i < lenof(sline_types); i++) {
	if (type == sline_types[i]) {
	    if (WallSlineExpire)
		wallops(s_OperServ, "S%cLINE on %s has expired",
			sline_types[i], md->mask);
	    cancel_sline((uint8)type, md->mask);
	}
    }
    return 0;
}

/*************************************************************************/
/***************************** Module stuff ******************************/
/*************************************************************************/

const int32 module_version = MODULE_VERSION_CODE;

ConfigDirective module_config[] = {
    { "ImmediatelySendSline",{{CD_SET, 0, &ImmediatelySendSline } } },
    { "SGlineExpiry",     { { CD_TIME, 0, &SGlineExpiry } } },
    { "SGlineReason",     { { CD_STRING, CF_DIRREQ, &SGlineReason } } },
    { "SQlineExpiry",     { { CD_TIME, 0, &SGlineExpiry } } },
    { "SQlineIgnoreOpers",{ { CD_SET, 0, &SQlineIgnoreOpers } } },
    { "SQlineKill",       { { CD_SET, 0, &SQlineKill } } },
    { "SQlineReason",     { { CD_STRING, CF_DIRREQ, &SQlineReason } } },
    { "SZlineExpiry",     { { CD_TIME, 0, &SGlineExpiry } } },
    { "SZlineReason",     { { CD_STRING, CF_DIRREQ, &SZlineReason } } },
    { "SlineDB",          { { CD_STRING, CF_DIRREQ, &SlineDBName } } },
    { "WallSlineExpire",  { { CD_SET, 0, &WallSlineExpire } } },
    { "WallOSSline",      { { CD_SET, 0, &WallOSSline } } },
    { NULL }
};

/*************************************************************************/

static int do_load_module(Module *mod, const char *modname)
{
    if (strcmp(modname, "nickserv/main") == 0) {
	module_nickserv = mod;
	if (!add_callback(mod, "REGISTER/LINK check", do_reglink_check))
	    module_log("Unable to register NickServ REGISTER/LINK check"
		       " callback");
    }
    return 0;
}

/*************************************************************************/

static int do_unload_module(Module *mod)
{
    if (mod == module_nickserv) {
	remove_callback(module_nickserv, "REGISTER/LINK check",
			do_reglink_check);
	module_nickserv = NULL;
    }
    return 0;
}

/*************************************************************************/

int init_module(Module *module_)
{
    module = module_;

    module_operserv = find_module("operserv/main");
    if (!module_operserv) {
	module_log("Main OperServ module not loaded");
	return 0;
    }
    use_module(module_operserv);

    if (!register_commands(module_operserv, cmds)) {
	module_log("Unable to register commands");
	exit_module(0);
	return 0;
    }

    cb_send_sgline   = register_callback(module, "send_sgline");
    cb_send_sqline   = register_callback(module, "send_sqline");
    cb_send_szline   = register_callback(module, "send_szline");
    cb_cancel_sgline = register_callback(module, "cancel_sgline");
    cb_cancel_sqline = register_callback(module, "cancel_sqline");
    cb_cancel_szline = register_callback(module, "cancel_szline");
    if (cb_send_sgline < 0 || cb_send_sqline < 0 || cb_send_szline < 0
     || cb_cancel_sgline < 0 || cb_cancel_sqline < 0 || cb_cancel_szline < 0
    ) {
	module_log("Unable to register callbacks");
	exit_module(0);
	return 0;
    }

    if (!add_callback(NULL, "load module", do_load_module)
     || !add_callback(NULL, "unload module", do_unload_module)
     || !add_callback(NULL, "user check", check_sline)
     || !add_callback(NULL, "user nickchange (after)", check_sqline_nickchange)
     || !add_callback(NULL, "save data", do_save_data)
     || !add_callback(module_operserv, "expire maskdata", do_expire_maskdata)
    ) {
	module_log("Unable to add callbacks");
	exit_module(0);
	return 0;
    }

    open_sline_db(SlineDBName);
    db_opened = 1;

    if (ImmediatelySendSline) {
	MaskData *sline;
	static uint8 types[] = {MD_SGLINE, MD_SQLINE, MD_SZLINE};
	int i;
	for (i = 0; i < lenof(types); i++) {
	    for (sline = first_maskdata(types[i]); sline;
		 sline = next_maskdata(types[i])
	    ) {
		send_sline(types[i], sline);
	    }
	}
    }

    return 1;
}

/*************************************************************************/

int exit_module(int shutdown_unused)
{
#ifdef CLEAN_COMPILE
    shutdown_unused = shutdown_unused;
#endif

    if (db_opened)
	close_sline_db(SlineDBName);

    if (module_nickserv)
	do_unload_module(module_nickserv);

    remove_callback(NULL, "save data", do_save_data);
    remove_callback(NULL, "user nickchange (after)", check_sqline_nickchange);
    remove_callback(NULL, "user check", check_sline);
    remove_callback(NULL, "unload module", do_unload_module);
    remove_callback(NULL, "load module", do_load_module);

    unregister_callback(module, cb_cancel_szline);
    unregister_callback(module, cb_cancel_sqline);
    unregister_callback(module, cb_cancel_sgline);
    unregister_callback(module, cb_send_szline);
    unregister_callback(module, cb_send_sqline);
    unregister_callback(module, cb_send_sgline);

    if (module_operserv) {
	remove_callback(module_operserv,"expire maskdata",do_expire_maskdata);
	unregister_commands(module_operserv, cmds);
	unuse_module(module_operserv);
	module_operserv = NULL;
    }

    return 1;
}

/*************************************************************************/
